File Coverage

blib/lib/WWW/GoDaddy/REST.pm
Criterion Covered Total %
statement 134 142 94.3
branch 25 34 73.5
condition 15 22 68.1
subroutine 27 27 100.0
pod 10 11 90.9
total 211 236 89.4


line stmt bran cond sub pod time code
1             package WWW::GoDaddy::REST;
2              
3 4     4   163376 use warnings;
  4         9  
  4         122  
4 4     4   18 use strict;
  4         5  
  4         114  
5              
6             #<<< NO perltidy - must be all on one line
7 4     4   1854 use version; our $VERSION = version->new('0.10');
  4         6563  
  4         20  
8             #>>>
9 4     4   382 use Carp qw(confess);
  4         7  
  4         298  
10 4     4   2062 use English qw( -no_match_vars );
  4         9897  
  4         25  
11 4     4   2596 use File::Slurp qw( slurp );
  4         22569  
  4         274  
12 4     4   2002 use LWP::UserAgent;
  4         121823  
  4         146  
13 4     4   32 use HTTP::Request;
  4         5  
  4         102  
14 4     4   2680 use Moose;
  4         1591894  
  4         35  
15 4     4   24546 use Moose::Util::TypeConstraints;
  4         9  
  4         36  
16 4     4   9004 use WWW::GoDaddy::REST::Resource;
  4         10  
  4         205  
17 4     4   2091 use WWW::GoDaddy::REST::Schema;
  4         12  
  4         159  
18 4     4   22 use WWW::GoDaddy::REST::Util qw(abs_url json_encode json_decode is_json );
  4         5  
  4         17  
19              
20             subtype 'PositiveInt', as 'Int', where { $_ > 0 };
21              
22 4     4   1639 no Moose::Util::TypeConstraints;
  4         24  
  4         36  
23              
24             my $JSON_MIME_TYPE = 'application/json';
25              
26             has 'url' => (
27             is => 'rw',
28             isa => 'Str',
29             required => 1,
30             documentation => 'Base url of the REST service'
31             );
32              
33             has 'timeout' => (
34             is => 'rw',
35             isa => 'PositiveInt',
36             required => 1,
37             default => 10,
38             documentation => 'Timeout in seconds for HTTP calls'
39             );
40              
41             has 'basic_username' => (
42             is => 'rw',
43             isa => 'Str',
44             required => 0,
45             documentation => 'Username portion if using basic auth'
46             );
47              
48             has 'basic_password' => (
49             is => 'rw',
50             isa => 'Str',
51             required => 0,
52             documentation => 'Password portion if using basic auth'
53             );
54              
55             has 'user_agent' => (
56             is => 'rw',
57             isa => 'Object',
58             required => 1,
59             default => \&default_user_agent,
60             );
61              
62             has 'schemas_file' => (
63             is => 'rw',
64             isa => 'Str',
65             required => 0,
66             documentation => 'Optional, cached copy of the schemas JSON to avoid HTTP round trip'
67             );
68              
69             has 'schemas' => (
70             is => 'rw',
71             isa => 'ArrayRef[WWW::GoDaddy::REST::Schema]',
72             required => 1,
73             lazy => 1,
74             builder => '_build_schemas'
75             );
76              
77             has 'raise_http_errors' => (
78             is => 'rw',
79             isa => 'Bool',
80             required => 1,
81             default => 1
82             );
83              
84             sub BUILD {
85 14     14 0 17502 my ( $self, $params ) = @_;
86              
87 14 100 100     124 if ( defined $params->{basic_username} or defined $params->{basic_password} ) {
88 3 100       12 if ( !defined $params->{basic_username} ) {
89 1         19 confess 'Attribute (basic_username) is required if basic_password is provided';
90             }
91 2 100       9 if ( !defined $params->{basic_password} ) {
92 1         30 confess 'Attribute (basic_password) is required if basic_username is provided';
93             }
94             }
95              
96 12 100 100     174 if ( defined $params->{schemas_file} and !-e $params->{schemas_file} ) {
97 1         34 confess 'Attribute (schemas_file) must be a file that exists: ' . $params->{schemas_file};
98             }
99              
100             }
101              
102             sub query {
103 8     8 1 3425 my $self = shift;
104 8         15 my $type = shift;
105 8         26 return $self->schema($type)->query(@_);
106             }
107              
108             sub query_by_id {
109 15     15 1 13737 my $self = shift;
110 15         36 my $type = shift;
111 15         59 return $self->schema($type)->query_by_id(@_);
112             }
113              
114             sub create {
115 3     3 1 4548 my $self = shift;
116 3         4 my $type = shift;
117 3         9 return $self->schema($type)->create(@_);
118             }
119              
120             sub schema {
121 42     42 1 980 my $self = shift;
122 42         58 my $name = shift;
123              
124 42         49 my @schemas = @{ $self->schemas() };
  42         1515  
125              
126 42         237 return WWW::GoDaddy::REST::Schema->registry_lookup($name);
127             }
128              
129             sub schemas_url {
130 100     100 1 115 my $self = shift;
131 100   100     217 my $specific_name = shift || '';
132              
133 100         3312 my $base = $self->url;
134 100         348 my $path = sprintf( 'schemas/%s', $specific_name );
135 100         293 return abs_url( $base, $path );
136             }
137              
138             sub http_request_schemas_json {
139 1     1 1 2 my $self = shift;
140              
141 1         4 my $request = $self->build_http_request( 'GET', $self->schemas_url );
142 1         31 my $response = $self->user_agent->request($request);
143 1         391 my $content = $response->content;
144 1 50 33     12 if ( !$response->is_success && $self->raise_http_errors ) {
145 0         0 die($content);
146             }
147 1         20 return $content;
148             }
149              
150             sub http_request_as_resource {
151 35     35 1 70 my ( $self, $method, $url, $content, $http_opts ) = @_;
152 35         112 my ( $struct_from_json, $http_response )
153             = $self->http_request( $method, $url, $content, $http_opts );
154              
155 28         245 my $resource = WWW::GoDaddy::REST::Resource->new_subclassed(
156             { client => $self,
157             fields => $struct_from_json,
158             http_response => $http_response
159             }
160             );
161              
162 28 50 33     31331 if ( !$http_response->is_success && $self->raise_http_errors ) {
163 0 0       0 if ($EXCEPTIONS_BEING_CAUGHT) {
164 0         0 die($resource);
165             }
166             else {
167 0         0 die( $resource->to_string );
168             }
169             }
170              
171 28         400 return $resource;
172             }
173              
174             sub http_request {
175 35     35 1 55 my $self = shift;
176 35         56 my ( $method, $uri, $perl_data, $http_opts ) = @_;
177              
178 35   100     106 $http_opts ||= {};
179 35   66     981 $http_opts->{timeout} ||= $self->timeout;
180              
181 35         987 $uri = abs_url( $self->url, $uri );
182              
183 35         2468 my $headers = undef;
184              
185 35         40 my $content;
186 35 100       86 if ( defined $perl_data ) {
187 10         18 $content = eval { json_encode($perl_data) };
  10         30  
188 10 50       21 if ($@) {
189 0         0 confess "$@:\n$perl_data";
190             }
191 10         28 $headers = [ 'Content-type' => $JSON_MIME_TYPE ];
192             }
193              
194 35         116 my $request = $self->build_http_request( $method, $uri, $headers, $content );
195              
196 35         51 my $response = eval {
197 35     6   437 local $SIG{ALRM} = sub { die("alarm\n") };
  6         5998608  
198 35         159 alarm $http_opts->{timeout};
199 35         1135 return $self->user_agent->request($request);
200             };
201 35         4533 alarm 0;
202 35 100       125 if ( my $e = $@ ) {
203 7 100       31 if ( $e eq "alarm\n" ) {
204 6         303 confess("timed out while calling '$method' '$uri'");
205             }
206             else {
207 1         18 confess($e);
208             }
209             }
210 28         69 my $response_text = $response->content;
211              
212 28         298 my $content_data;
213 28 100       49 if ($response_text) {
214 27         35 $content_data = eval { json_decode($response_text) };
  27         73  
215 27 50       76 if ($@) {
216 0         0 confess "$@:\n$response_text";
217             }
218             }
219             else {
220 1         2 $content_data = undef;
221             }
222 28 50       142 return wantarray ? ( $content_data, $response ) : $content_data;
223             }
224              
225             sub build_http_request {
226 36     36 1 385 my $self = shift;
227 36         88 my @params = @_;
228              
229 36         206 my $request = HTTP::Request->new(@params);
230 36 50 33     4046 if ( defined $self->basic_username or defined $self->basic_password ) {
231 0         0 $request->authorization_basic( $self->basic_username, $self->basic_password );
232             }
233 36         85 return $request;
234             }
235              
236             sub default_user_agent {
237 11     11 1 23737 my $ua = LWP::UserAgent->new( env_proxy => 1 );
238 11         34097 $ua->default_headers->push_header( 'Accept' => $JSON_MIME_TYPE );
239 11         327 return $ua;
240             }
241              
242             sub _build_schemas {
243 4     4   11 my $self = shift;
244              
245 4         9 my $schema_json;
246 4 100       112 if ( $self->schemas_file ) {
247 3         76 $schema_json = slurp( $self->schemas_file );
248             }
249             else {
250 1         5 $schema_json = $self->http_request_schemas_json;
251             }
252              
253 4         401 my $struct = eval { json_decode($schema_json) };
  4         27  
254 4 50       18 if ($@) {
255 0         0 confess "$@:\n$schema_json";
256             }
257 4         6 foreach my $schema_struct ( @{ $struct->{data} } ) {
  4         49  
258 44         225 my $schema = WWW::GoDaddy::REST::Resource->new_subclassed(
259             { client => $self, fields => $schema_struct } );
260 44         40380 my $key = $schema->link('self');
261 44         137 WWW::GoDaddy::REST::Schema->registry_add( $schema->id => $schema );
262 44         102 WWW::GoDaddy::REST::Schema->registry_add( $key => $schema );
263             }
264              
265 4         19 return [ WWW::GoDaddy::REST::Schema->registry_list ];
266             }
267              
268             1;
269              
270             =head1 NAME
271              
272             WWW::GoDaddy::REST - Work with services conforming to the GDAPI spec
273              
274             =head1 SYNOPSIS
275              
276             use WWW::GoDaddy::REST;
277              
278             my $client = WWW::GoDaddy::REST->new({
279             url => 'https://example.com/v1',
280             basic_username => 'theuser',
281             basic_password => 'notsosecret'
282             });
283              
284             # see docs for WWW::GoDaddy::REST::Resource for more info
285             my $auto = $client->query_by_id('autos',$vehicle_id_number);
286              
287             print $auto->f('make'); # get a field
288             print $auto->f('model','S'); # set a field
289             $saved_auto = $auto->save();
290              
291             my $resource = $auto->follow_link('link_name');
292             my $resource = $auto->do_action('drive', { lat => ..., lon => ...});
293              
294             my $new = $client->create('autos', { 'make' => 'Tesla', 'model' => 'S' });
295              
296             $auto->delete();
297              
298             my @autos = $client->query('autos',{ 'make' => 'tesla' });
299              
300             =head1 DESCRIPTION
301              
302             This client makes it easy to code against a REST API that is created using
303             the Go Daddy (r) API Specification (GDAPI) L<https://github.com/godaddy/gdapi>.
304              
305             You will typically only need three pieces of information:
306             - base url of the api (this must include the version number)
307             - username
308             - password
309              
310             =head1 SEARCHING AND FILTERS
311              
312             There are two methods that deal with searching: C<query> and C<query_by_id>.
313              
314             =head2 SEARCH BY ID
315              
316             Example:
317              
318             # GET /v1/how_the_schema_defines/the_resource/url/id
319             $resource = $client->query_by_id('the_schema','the_id');
320              
321             # GET /v1/how_the_schema_defines/the_resource/url/id?other=param
322             $resource = $client->query_by_id('the_schema','the_id', { other => 'param' });
323              
324             =head2 SEARCH WITH FILTER
325              
326             Filters are hash references. The first level key is the field
327             name that you are searching on. The value of the field is an array
328             reference that has a list of hash references.
329              
330             Full Syntax Example:
331              
332             @items = $client->query( 'the_schema_name',
333             {
334             'your_field_name' => [
335             {
336             'modifier' => 'your modifier like "eq" or "ne"',
337             'value' => 'your search value'
338             },
339             {
340             #...
341             },
342             ],
343             'another_field' => ...
344             }
345             );
346              
347             Now there are shortcuts as well.
348              
349             Single Field Equality Example:
350              
351             @items = $client->query( 'the_schema_name',
352             { 'your_field_name' => 'your search value' }
353             );
354              
355             Assumed Equality Example:
356              
357             @items = $client->query( 'the_schema_name',
358             {
359             'your_field_name' => [
360             {
361             # notice the missing 'modifier' key
362             'value' => 'your search value',
363             }
364             ],
365             'another_field' => 'equality search too'
366             }
367             );
368              
369             Pass Through to query_by_id VS Search
370              
371             $resource = $client->query( 'the_schema_name', 'id' );
372              
373             =head1 ATTRIBUTES
374              
375             Attributes can be provided in the C<new> method and have corresponding
376             methods to get/set the values.
377              
378             =over 4
379              
380             =item url
381              
382             Base URL for the web service. This must include the version portion of the
383             URL as well.
384              
385             Trailing slash can be present or left out.
386              
387             Example:
388              
389             $c = WWW::GoDaddy::REST->new( {
390             url => 'https://example.com/v1'
391             } );
392              
393             =item basic_username
394              
395             The username or key you were assigned for the web service.
396              
397             Example:
398              
399             $c = WWW::GoDaddy::REST->new( {
400             url => '...',
401             basic_username => 'me',
402             basic_password => '...'
403             } );
404              
405             NOTE: not all web services authenticate using HTTP Basic Auth. In this case,
406             you will need to provide your own C<user_agent> with default headers to
407             accomplish authentication on your own.
408              
409             =item basic_password
410              
411             The password or secret you were assigned for the web service.
412              
413             Example:
414              
415             $c = WWW::GoDaddy::REST->new( {
416             url => '...',
417             basic_username => '...',
418             basic_password => 'very_secret'
419             } );
420              
421             NOTE: not all web services authenticate using HTTP Basic Auth. In this case,
422             you will need to provide your own C<user_agent> with default headers to
423             accomplish authentication on your own.
424              
425             =item user_agent
426              
427             The instance of L<LWP::UserAgent> that is used for all HTTP(S) interraction.
428              
429             This has a sane default if you do not provide an instance yourself.
430              
431             You may override this if you wish in the constructor or later on at runtime.
432              
433             See the C<default_user_agent> in L<"CLASS METHODS">.
434              
435             Example:
436              
437             $ua = LWP::UserAgent->new();
438             $ua->default_headers->push_header(
439             'Authorization' => 'MyCustom ASDFDAFFASFASFSAFSDFAS=='
440             );
441             $c = WWW::GoDaddy::REST->new({
442             url => '...',
443             user_agent => $ua
444             });
445              
446             =item schemas_file
447              
448             Optional path to a file containing the JSON for all of the schemas for this web
449             service (from the schemas collection). If you would like to avoid a round trip
450             to the server at runtime, this is the way to do it.
451              
452             Example:
453              
454             $c = WWW::GoDaddy::REST->new({
455             url => '...',
456             schemas_file => '/my/app/schema.json'
457             });
458              
459             See the GDAPI Specification for more information about schemas and collections.
460             L<https://github.com/godaddy/gdapi/blob/master/specification.md>
461              
462             =item raise_http_errors
463              
464             Boolean value that indicates whether a C<die()> will occur in the
465             event of a non successful HTTP response (4xx 5xx etc).
466              
467             It defaults to True. Set to a false value if you wish to check
468             the HTTP response code in the resultant resource on your own.
469              
470             =back
471              
472             =head1 METHODS
473              
474             =over 4
475              
476             =item query
477              
478             Search for a list of resources given a schema name and a filter.
479              
480             If the second parameter is a scalar, is assumes you are not searching but rather
481             trying to load a specific resource.
482              
483             See the documentation for C<query_by_id>.
484              
485             In scalar context, this returns a L<Collection|WWW::GoDaddy::REST::Collection>
486             object.
487              
488             In list context, this returns a list of L<Resource|WWW::GoDaddy::REST::Resource>
489             objects (or subclasses).
490              
491              
492             Example:
493              
494             @items = $client->query('schema_name',{ 'field' => 'value' });
495             $collection = $client->query('schema_name',{ 'field' => 'value' });
496             $item = $client->query('schema_name','1234');
497             $item = $client->query('schema_name','1234',{ 'field' => 'value' });
498             $item = $client->query('schema_name','1234',undef,{ timeout => 15 });
499              
500             See L<"SEARCHING AND FILTERS"> for more information.
501              
502             =item query_by_id
503              
504             Search for a single instance of a resource by its primary id. Optionally
505             specify a hash for additional query params to append to the resource URL.
506              
507             This returns a L<Resource|WWW::GoDaddy::REST::Resource> (or a subclass).
508              
509             Example:
510              
511             # GET /v1/how_the_schema_defines/the_resource/url/the_id
512             $resource = $client->query_by_id('the_schema','the_id');
513             $resource = $client->query_by_id('the_schema','the_id',undef,{ timeout => 15 });
514              
515             # GET /v1/how_the_schema_defines/the_resource/url/the_id?other=param
516             $resource = $client->query_by_id('the_schema','the_id', { other => 'param' });
517             $resource = $client->query_by_id('the_schema','the_id', { other => 'param' }, { timeout => 15 });
518              
519             =item create
520              
521             Given a schema and a resource (or hashref), a POST will be made
522             against the collection url of the schema to create the resource.
523              
524             This returns a L<WWW::GoDaddy::REST::Resource> (or a subclass).
525              
526             Example:
527              
528             $car = $client->create('autos', { 'make' => 'Tesla', 'model' => 'S' });
529             $car = $client->create('autos', { 'make' => 'Tesla', 'model' => 'S' }, { timeout => 30 });
530              
531             =item schema
532              
533             Given a schema name, return a L<WWW::GoDaddy::REST::Schema> object or
534             undef if it is not found.
535              
536             Example:
537              
538             $schema_resource = $client->schema('the_schema');
539              
540             =item schemas_url
541              
542             If no schema name is provided, return the schema collection url where you can
543             retrieve the collection of all schemas.
544              
545             If a schema name is provided, return the URL where you can retrieve the schema
546             with the given name.
547              
548             Example:
549              
550             $c = WWW::GoDaddy::REST->new({url => 'http://example.com/v1/'});
551             $c->schemas_url(); # http://example.com/v1/schemas/
552             $c->schemas_url('error'); # http://example.com/v1/schemas/error
553              
554             =item http_request
555              
556             Perform the HTTP request and return a hashref of the decoded JSON response.
557              
558             If this is called in list context, it returns the decoded JSON response and
559             the associated L<HTTP::Response> object.
560              
561             This takes the following parameters (similar but not the same as L<HTTP::Request>):
562             - HTTP method
563             - URL relative to the web service base C<url>
564             - Optional hashref of data to send as JSON content
565              
566             The url provided will be rooted to the base url, C<url>.
567              
568             Example:
569              
570             $c = WWW::GoDaddy::REST->new({
571             url => 'http://example.com/v1/'
572             });
573              
574             # GET http://example.com/v1/servers/Asdf
575             $data_hashref = $c->http_request('GET','/servers/Asdf')
576              
577             ($hash,$http_response) = $c->http_request('GET','/servers/Asdf');
578              
579             =item http_request_as_resource
580              
581             Perform the HTTP request and return a L<WWW::GoDaddy::REST::Resource> instance.
582              
583             This takes the following parameters (similar but not the same as L<HTTP::Request>):
584             - HTTP method
585             - URL relative to the web service base C<url>
586             - Optional hashref of data to send as JSON content
587              
588             The url provided will be rooted to the base url, C<url>.
589              
590             The url provided will be rooted to the base url, C<url>.
591              
592             Example:
593              
594             $c = WWW::GoDaddy::REST->new({
595             url => 'http://example.com/v1/'
596             });
597              
598             # GET http://example.com/v1/servers/Asdf
599             $resource = $c->http_request_as_resource('GET','/servers/Asdf')
600              
601             =item http_request_schemas_json
602              
603             Retrieve the JSON string for the schemas collection.
604              
605             Example:
606              
607             $c = WWW::GoDaddy::REST->new({
608             url => 'http://example.com/v1/'
609             });
610              
611             $schemas_json = $c->http_request_schemas_json();
612             # write this out to a file for later use
613             # with the 'schemas_file' parameter for example
614              
615              
616             =item build_http_request
617              
618             Given parameters for a L<HTTP::Request> object, return an instance
619             of this object with certain defaults filled in.
620              
621             As of this writing the defaults filled in are:
622              
623             - HTTP basic auth headers if auth is provided
624              
625             Unlike other methods such as C<http_request>, the C<url> is not rooted
626             to the base url.
627              
628             Example:
629              
630            
631             $c = WWW::GoDaddy::REST->new({
632             url => 'http://example.com/v1/'
633             });
634              
635             $request = $c->build_http_request('GET','http://example.com/v1/test');
636              
637             =back
638              
639             =head1 CLASS METHODS
640              
641             =over 4
642              
643             =item default_user_agent
644              
645             Generate a default L<LWP::UserAgent>. See C<user_agent>.
646              
647             Example:
648              
649             $ua = WWW::GoDaddy::REST->default_user_agent();
650             $ua->default_headers->push('X-Custom' => 'thing');
651             $c = WWW::GoDaddy::REST->new({
652             user_agent => $ua,
653             url => '...'
654             });
655              
656             =back
657              
658             =head1 SEE ALSO
659              
660             C<gdapi-shell> command line program.
661              
662             =head1 AUTHOR
663              
664             David Bartle, C<< <davidb@mediatemple.net> >>
665              
666             =head1 COPYRIGHT & LICENSE
667              
668             Copyright (c) 2014 Go Daddy Operating Company, LLC
669              
670             Permission is hereby granted, free of charge, to any person obtaining a
671             copy of this software and associated documentation files (the "Software"),
672             to deal in the Software without restriction, including without limitation
673             the rights to use, copy, modify, merge, publish, distribute, sublicense,
674             and/or sell copies of the Software, and to permit persons to whom the
675             Software is furnished to do so, subject to the following conditions:
676              
677             The above copyright notice and this permission notice shall be included in
678             all copies or substantial portions of the Software.
679              
680             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
681             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
682             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
683             THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
684             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
685             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
686             DEALINGS IN THE SOFTWARE.
687              
688             =cut