File Coverage

blib/lib/WWW/GoDaddy/REST/Resource.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


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