File Coverage

blib/lib/WWW/GoDaddy/REST/Resource.pm
Criterion Covered Total %
statement 171 175 97.7
branch 44 50 88.0
condition 21 38 55.2
subroutine 34 35 97.1
pod 25 25 100.0
total 295 323 91.3


line stmt bran cond sub pod time code
1             package WWW::GoDaddy::REST::Resource;
2              
3 4     4   18 use Carp;
  4         6  
  4         284  
4 4     4   17 use List::MoreUtils qw( natatime );
  4         6  
  4         34  
5 4     4   1164 use Moose;
  4         5  
  4         29  
6 4     4   20813 use URI;
  4         9  
  4         116  
7 4     4   2064 use URI::QueryParam;
  4         2190  
  4         118  
8 4     4   1502 use WWW::GoDaddy::REST::Util qw( abs_url json_instance json_encode json_decode is_json );
  4         12  
  4         32  
9              
10 4     4   1986 use constant DEFAULT_IMPL_CLASS => 'WWW::GoDaddy::REST::Resource';
  4         9  
  4         248  
11 4     4   27 use overload '""' => \&to_string;
  4         7  
  4         34  
12              
13             has 'client' => (
14             is => 'rw',
15             isa => 'WWW::GoDaddy::REST',
16             required => 1
17             );
18              
19             has 'fields' => (
20             is => 'ro',
21             isa => 'HashRef',
22             required => 1,
23             );
24              
25             has 'http_response' => (
26             is => 'ro',
27             isa => 'Maybe[HTTP::Response]',
28             required => 0
29             );
30              
31             sub save {
32 2     2 1 168 my $self = shift;
33 2         4 my $http_opts = shift;
34 2   33     8 my $url = $self->link('self') || $self->client->schema( $self->type )->query_url( $self->id );
35 2         79 return $self->client->http_request_as_resource( 'PUT', $url, $self, $http_opts );
36             }
37              
38             sub delete {
39 2     2 1 608 my $self = shift;
40 2         4 my $http_opts = shift;
41 2   33     6 my $url = $self->link('self') || $self->client->schema( $self->type )->query_url( $self->id );
42 2         58 return $self->client->http_request_as_resource( 'DELETE', $url, $self, $http_opts );
43             }
44              
45             sub follow_link {
46 2     2 1 34 my $self = shift;
47 2         3 my $link_name = shift;
48 2         4 my $http_opts = shift;
49              
50 2         4 my $link_url = $self->link($link_name);
51 2 100       8 if ( !$link_url ) {
52 1         2 my @valid_links = keys %{ $self->links() };
  1         3  
53 1         27 croak("$link_name is not a valid link name. Did you mean one of these? @valid_links");
54             }
55              
56 1         43 return $self->client->http_request_as_resource( 'GET', $link_url, undef, $http_opts );
57             }
58              
59             sub do_action {
60 5     5 1 42 my $self = shift;
61 5         8 my $action = shift;
62 5         6 my $params = shift;
63 5         9 my $http_opts = shift;
64              
65 5         11 my $action_url = $self->action($action);
66 5 100       12 if ( !$action_url ) {
67 2 50       7 if ( $self->id ) {
68              
69             # try and find an action in the schema as fallback
70 2         5 my $schema = $self->schema();
71 2   50     7 my $resource_actions = $schema->f('resourceActions') || {};
72 2 100       6 if ( exists $resource_actions->{$action} ) {
73 1   33     3 my $self_uri = URI->new( $self->link('self') || $schema->query_url( $self->id ) );
74 1         292 $self_uri->query("$action");
75 1         18 $action_url = "$self_uri";
76             }
77             }
78 2 100       11 if ( !$action_url ) {
79 1         21 croak("$action is not a valid action name.");
80             }
81             }
82              
83 4         140 return $self->client->http_request_as_resource( 'POST', $action_url, $params, $http_opts );
84              
85             }
86              
87             sub items {
88 0     0 1 0 my $self = shift;
89 0         0 return ($self);
90             }
91              
92             sub id {
93 165     165 1 1345 return shift->f('id');
94             }
95              
96             sub type {
97 10     10 1 1345 return shift->f('type');
98             }
99              
100             sub resource_type {
101 10     10 1 295 return shift->f('resourceType');
102             }
103              
104             sub type_fq {
105 4     4 1 6 my $self = shift;
106              
107 4         10 my $base_url = $self->schemas_url;
108              
109 4         852 return abs_url( $base_url, $self->type );
110             }
111              
112             sub resource_type_fq {
113 5     5 1 8 my $self = shift;
114              
115 5 100       13 return unless $self->resource_type;
116              
117 1         5 return abs_url( $self->schemas_url, $self->resource_type );
118             }
119              
120             sub schema {
121 5     5 1 10 my $self = shift;
122 5   33     146 my $schema
123             = $self->client->schema( $self->resource_type_fq )
124             || $self->client->schema( $self->type_fq )
125             || $self->client->schema( $self->type );
126 5         23 return $schema;
127             }
128              
129             sub link {
130 83     83 1 134 my $self = shift;
131 83         125 my $name = shift;
132              
133 83         217 my $links = $self->links();
134 83 100       268 if ( exists $links->{$name} ) {
135 79         225 return $links->{$name};
136             }
137 4         11 return undef;
138             }
139              
140             sub links {
141 84     84 1 180 return shift->f('links');
142             }
143              
144             sub action {
145 5     5 1 8 my $self = shift;
146 5         6 my $name = shift;
147              
148 5         12 my $actions = $self->actions();
149 5 100       14 if ( exists $actions->{$name} ) {
150 3         5 return $actions->{$name};
151             }
152 2         6 return undef;
153             }
154              
155             sub actions {
156 5   100 5 1 11 return shift->f('actions') || {};
157             }
158              
159             sub f {
160 348     348 1 121954 return shift->field(@_);
161             }
162              
163             sub f_as_resources {
164 7     7 1 2195 my $self = shift;
165 7         14 my $field = shift;
166 7         15 my $raw_data = $self->f($field);
167              
168 7         11 my ( $container, $type );
169              
170             # if the 'field' is data, skip detection and use the resource type
171 7 100       21 if ( $field eq 'data' ) {
172 4         22 ( $container, $type ) = ( 'array', $self->resource_type );
173             }
174             else {
175 3         9 ( $container, $type ) = $self->schema->resource_field_type($field);
176             }
177 7         246 my %defaults = (
178             client => $self->client,
179             http_response => $self->http_response
180             );
181              
182 7         193 my $type_schema = $self->client->schema($type);
183 7 100       106 if ($type_schema) {
184 6 100 100     61 if ( $container && $container eq 'map' ) {
    100 66        
    50          
185 1         1 my %ret;
186 1         2 foreach ( my ( $k, $v ) = each %{$raw_data} ) {
  1         5  
187 2   66     830 $v->{type} ||= $type_schema->id;
188 2         10 $ret{$k} = $self->new_subclassed( { %defaults, fields => $v } );
189             }
190 1         1116 return \%ret;
191             }
192             elsif ( $container && $container eq 'array' ) {
193 4         9 my @ret;
194 4         6 foreach my $v ( @{$raw_data} ) {
  4         11  
195 23   33     17989 $v->{type} ||= $type_schema->id;
196 23         101 push @ret, $self->new_subclassed( { %defaults, fields => $v } );
197             }
198 4         3522 return \@ret;
199             }
200             elsif ( ref($raw_data) eq 'HASH' ) {
201 1         2 my %ret = %{$raw_data};
  1         4  
202 1   33     5 $ret{type} ||= $type_schema->id;
203 1         7 return $self->new_subclassed( { %defaults, fields => \%ret } );
204             }
205             }
206              
207             # just return the raw data if not returned otherwise
208 1         4 return $raw_data;
209              
210             }
211              
212             sub field {
213 351     351 1 348 my $self = shift;
214 351 100       639 if ( @_ <= 1 ) {
215 346         630 return $self->_get_field(@_);
216             }
217             else {
218 5         15 return $self->_set_field(@_);
219             }
220             }
221              
222             sub schemas_url {
223 5     5 1 7 my $self = shift;
224              
225 5         6 my $found;
226              
227 5         139 my $http_resp = $self->http_response;
228 5         13 my $link = $self->link('schemas');
229              
230 5 50 66     29 if ( $http_resp && $http_resp->header('X-API-Schemas') ) {
231 0         0 return $http_resp->header('X-API-Schemas');
232             }
233              
234 5         267 return $self->client->schemas_url;
235             }
236              
237             sub _get_field {
238 346     346   315 my $self = shift;
239 346         394 my $name = shift;
240              
241 346 100       11678 if ( !exists $self->fields->{$name} ) {
242 8         36 return undef;
243             }
244 338         10213 return $self->fields->{$name};
245              
246             }
247              
248             sub _set_field {
249 5     5   8 my $self = shift;
250 5         9 my $name = shift;
251 5         8 my $value = shift;
252 5         177 $self->fields->{$name} = $value;
253 5         147 return $self->fields->{$name};
254             }
255              
256             sub new_subclassed {
257 98     98 1 127 my $class = shift;
258 98         101 my $params = shift;
259              
260 98         127 my $data = $params->{fields};
261              
262 98         128 my $impl = DEFAULT_IMPL_CLASS;
263 98 100       270 if ( ref($data) eq "HASH" ) {
264 94   50     281 my $type_short = $params->{fields}->{type} || '';
265 94 50       356 my $type_long = $type_short ? $params->{client}->schemas_url($type_short) : '';
266              
267 94   100     44354 $impl = $class->find_implementation( ( $type_long, $type_short ) ) || DEFAULT_IMPL_CLASS;
268 94         5924 eval "require $impl;";
269             }
270             else {
271              
272             # hmm, the json response didn't seem to be a hashref...
273             # well in this case, there are not really any fields
274             # so the caller will have to check the http_response
275             # content
276 4         8 $params->{fields} = {};
277             }
278              
279 98         598 return $impl->new($params);
280              
281             }
282              
283             sub TO_JSON {
284 32     32 1 33 my $self = shift;
285 32         71 return $self->data;
286             }
287              
288             sub data {
289 35     35 1 202 my $self = shift;
290              
291 35 100       29 my %fields = %{ $self->fields || {} };
  35         1122  
292 35 100       296 if (%fields) {
    100          
293 31         650 return \%fields;
294             }
295             elsif ( $self->http_response ) {
296 3         80 my $content = $self->http_response->decoded_content;
297 3 50       283 return is_json($content) ? json_decode($content) : $content;
298             }
299 1         11 return {};
300             }
301              
302             sub to_string {
303 28     28 1 3536 my $self = shift;
304 28         25 my $pretty = shift;
305              
306 28         69 my $JSON = json_instance();
307 28 50       50 if ($pretty) {
308 0         0 $JSON->pretty(1);
309             }
310 28         57 return json_encode( $self, $JSON );
311             }
312              
313             my %SCHEMA_TO_IMPL = (
314             'collection' => 'WWW::GoDaddy::REST::Collection',
315             'schema' => 'WWW::GoDaddy::REST::Schema',
316             );
317              
318             sub find_implementation {
319 100     100 1 1901 my $class = shift;
320 100         172 my @look_for = @_;
321              
322 100         168 foreach (@look_for) {
323 194 100       1126 if ( exists $SCHEMA_TO_IMPL{$_} ) {
324 63         260 return $SCHEMA_TO_IMPL{$_};
325             }
326             }
327 37         182 return;
328             }
329              
330             sub register_implementation {
331 3     3 1 1396 my $class = shift;
332              
333 3 100       12 if ( @_ % 2 != 0 ) {
334 1         23 croak("Expecting even number of parameters");
335             }
336              
337 2         20 my $iterator = natatime 2, @_;
338 2         22 while ( my ( $schema, $subclass ) = $iterator->() ) {
339 3         11 $SCHEMA_TO_IMPL{$schema} = $subclass;
340             }
341 2         13 return;
342             }
343              
344             1;
345              
346             =head1 NAME
347              
348             WWW::GoDaddy::REST::Resource - Represent a REST resource
349              
350             =head1 SYNOPSIS
351              
352             $client = WWW::GoDaddy::REST->new(...);
353              
354             $resource = WWW::GoDaddy::REST::Resource->new({
355             client => $client,
356             fields => {
357             'type' => 'automobile',
358             'id' => '1001',
359             'make' => 'Tesla',
360             'model' => 'S'
361             'links' => {
362             'self' => 'https://example.com/v1/automobiles/1001',
363             'schemas' => 'https://example.com/v1/schemas'
364             },
365             'actions' => {
366             'charge' => 'https://example.com/v1/automobiles/1001?charge'
367             }
368             # ...
369             # see: https://github.com/godaddy/gdapi/blob/master/specification.md
370             },
371             });
372              
373             $resource->f('id'); # get 1001
374             $resource->f('id','2000'); # set to 2000 and return 2000
375              
376             # follow a link in links section
377             $schemas_resource = $resource->follow_link('schemas');
378              
379             # perform an action in the actions section
380             $result_resource = $resource->do_action('charge',{ 'with' => 'quick_charger' });
381              
382             =head1 DESCRIPTION
383              
384             Base class used to represent a REST resource.
385              
386             =head1 CLASS METHODS
387              
388             =over 4
389              
390             =item new
391              
392             Given a hash reference of L<"ATTRIBUTES"> and values, return a new instance
393             of this object.
394              
395             It is likely more important that you use the C<new_subclassed> class method.
396              
397             Example:
398              
399             my $resource = WWW::GoDaddy::REST::Resource->new({
400             client => WWW::GoDaddy::REST->new(...),
401             fields => {
402             id => '...',
403             ...
404             },
405             });
406              
407             =item new_subclassed
408              
409             This takes the same paramegers as C<new> and is the preferred construction
410             method. This tries to find the appropriate subclass of
411             C<WWW::GoDaddy::REST::Resource> and passes along the paramegers to the C<new>
412             method of that subclass instead.
413              
414             See also: C<new>
415              
416             =item find_implementation
417              
418             Given a list of schema type names, find the best implementation sub class.
419              
420             Returns the string of the class name. If no good subclass candidate is found,
421             returns undef.
422              
423             Example:
424              
425             find_implementation( 'schema' );
426             # WWW::GoDaddy::REST::Schema
427              
428             =item register_implementation
429              
430             Register a subclass handler for a schema type given a schema name and the
431             name of a L<WWW::GoDaddy::REST::Resource> subclass.
432              
433             This can take as many schema => resource class pairs as you want.
434              
435             Example:
436              
437             WWW::GoDaddy::REST::Resource->register_subclass( 'account' => 'My::AccountRes' );
438             WWW::GoDaddy::REST::Resource->register_subclass( 'foo' => 'Bar', 'baz' => 'Buzz' );
439              
440             =back
441              
442             =head1 ATTRIBUTES
443              
444             =over 4
445              
446             =item client
447              
448             Instance of L<WWW::GoDaddy::REST> associated with the resource.
449              
450             =item fields
451              
452             Hash reference containing the raw data for the underlying resource.
453              
454             Several methods delegate to this underlying structure such as C<f>,
455             and C<field>.
456              
457             =item http_response
458              
459             Optionally present instance of an L<HTTP::Response> object so that
460             you can inspect the HTTP information related to the resource.
461              
462             =back
463              
464             =head1 METHODS
465              
466             =over 4
467              
468             =item f
469              
470             Get or set a field by name. You may also use the longer name C<field>.
471              
472             When performing a set, it also returns the new value that was set.
473              
474             Example:
475              
476             $res->f('field_name'); # get
477             $res->f('field_name','new'); # set
478              
479             =item f_as_resources
480              
481             Get a field by name. If it is a resource, this will turn it into an
482             object instead of giving you the raw hash reference as the return value.
483              
484             Note, if the field is a 'map' or 'array' of resources, every item in
485             those lists will be 'resourcified'.
486              
487             If this is not a resource, then it does return the raw value.
488              
489             Example:
490              
491             # return value is a WWW::GoDaddy::REST::Resource, not a hash ref
492             $driver = $car->f('driver');
493              
494             See C<f> if you want the raw value. This will return the raw value, if
495             the value does not look like a resource.
496              
497             =item field
498              
499             Get or set a field by name. You may also use the shorter name C<f>.
500              
501             When performing a set, it also returns the new value that was set.
502              
503             Example:
504              
505             $res->field('field_name'); # get
506             $res->fieldf('field_name','new'); # set
507              
508             =item save
509              
510             Does a PUT at this resources URI. Returns a new resource object.
511              
512             Example:
513              
514             $r2 = $r1->save();
515              
516             =item delete
517              
518             Does a DELETE on this resource. Returns a new resource object. This
519             return value likely is only useful to get at the C<http_response> attribute.
520              
521             =item do_action
522              
523             Does a POST with the supplied data on the action URL with the given name.
524              
525             If the action with the provided name does not exist, this method will
526             die. See also: C<action> and C<actions>
527              
528             Example:
529              
530             $r2 = $r1->do_action('some_action',{ a => 'a_v' });
531              
532             =item follow_link
533              
534             Gets the resource by following the link URL with the provided name.
535              
536             If the link with the provided name does not exist, this method will
537             die. See also: C<link> and C<link>
538              
539             Example:
540              
541             $r2 = $r1->follow_link('some_link');
542              
543             =item id
544              
545             Return the id of this instance
546              
547             =item type
548              
549             Return the name of the schema type that this object belongs to.
550              
551             =item type_fq
552              
553             Return the full URI to the schema type that this object belongs to.
554              
555             =item resource_type
556              
557             Return the name of the schema type that this collection's objects belong to.
558              
559             =item resource_type_fq
560              
561             Return the full URI to the schema type that this collection's objects belong to.
562              
563             =item schemas_url
564              
565             Returns the URL for the schema collection. This differs from the
566             client C<schemas_url> method since it has more places to look for hints
567             of the schemas collection url (headers, json response etc).
568              
569             Example:
570              
571             $r->schemas_url();
572              
573             =item schema
574              
575             Find and return the L<WWW::GoDaddy::REST::Schema> object that this is.
576              
577             =item link
578              
579             Return the link URL for the given name or undef if it does not exist.
580              
581             Example:
582              
583             # https://example.com/v1/thing/...
584             $r->link('self');
585             # 'https://example.com/v1/me/1'
586              
587             =item links
588              
589             Return the hashref that contains the link => url information
590              
591             Example:
592              
593             $r->links();
594             # {
595             # 'self' => 'https://example.com/v1/me/1',
596             # 'some_link' => 'https://example.com/v1/me/1/some_link'
597             # }
598              
599             =item action
600              
601             Return the action URL for the given name.
602              
603             Example:
604              
605             $r->action('custom_action');
606             # https://example.com/v1/thing/1001?some_action
607              
608             =item actions
609              
610             Return the hashref that contains the action => url information
611              
612             Example:
613              
614             $r->actions();
615             # {
616             # 'custom_action' => 'https://example.com/v1/thing/1001?some_action'
617             # }
618              
619             =item items
620              
621             Returns a list of resources that this resource contains. This implementation
622             simply returns a list of 'self'. It is here to be consistent with the
623             implementation found in L<WWW::GoDaddy::REST::Collection>.
624              
625             Example:
626              
627             @items = $resource->items();
628              
629             =item TO_JSON
630              
631             Returns a hashref that represents this object. This exists to make using the
632             L<JSON> module more convenient. This does NOT return a JSON STRING, just a
633             perl data structure.
634              
635             See C<to_string>.
636              
637             =item to_string
638              
639             Returns a JSON string that represents this object. This takes an optional
640             parameter, "pretty". If true, the json output will be prettified. This defaults
641             to false.
642              
643             =item data
644              
645             The resource is returned as a perl data structure. Note, if there are no
646             C<fields>, then the http_respons is consulted, if json data is found in
647             the content, that is returned (for instance, a plane old string or integer).
648              
649             =back
650              
651             =head1 AUTHOR
652              
653             David Bartle, C<< <davidb@mediatemple.net> >>
654              
655             =head1 COPYRIGHT & LICENSE
656              
657             Copyright (c) 2014 Go Daddy Operating Company, LLC
658              
659             Permission is hereby granted, free of charge, to any person obtaining a
660             copy of this software and associated documentation files (the "Software"),
661             to deal in the Software without restriction, including without limitation
662             the rights to use, copy, modify, merge, publish, distribute, sublicense,
663             and/or sell copies of the Software, and to permit persons to whom the
664             Software is furnished to do so, subject to the following conditions:
665              
666             The above copyright notice and this permission notice shall be included in
667             all copies or substantial portions of the Software.
668              
669             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
670             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
671             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
672             THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
673             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
674             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
675             DEALINGS IN THE SOFTWARE.
676              
677              
678             =cut