File Coverage

blib/lib/WWW/Spotify/Endpoint.pm
Criterion Covered Total %
statement 23 190 12.1
branch 0 122 0.0
condition 0 59 0.0
subroutine 8 16 50.0
pod 0 5 0.0
total 31 392 7.9


line stmt bran cond sub pod time code
1             package WWW::Spotify::Endpoint;
2             our $VERSION = '0.014';
3 4     4   2732 use 5.012;
  4         20  
4 4     4   22 use strict;
  4         5  
  4         127  
5 4     4   18 use warnings;
  4         6  
  4         250  
6              
7 4     4   22 use Moo::Role;
  4         15  
  4         26  
8              
9 4     4   2006 use JSON::MaybeXS qw( encode_json );
  4         10  
  4         290  
10 4     4   48 use HTTP::Status qw( HTTP_OK HTTP_NO_CONTENT HTTP_CREATED HTTP_ACCEPTED );
  4         1687  
  4         309  
11 4     4   1803 use URI::Escape qw( uri_escape );
  4         20538  
  4         16229  
12              
13             #--------------------------------------------------------------------------
14             # Generic HTTP verb helpers extracted from the main module. They rely on
15             # a number of attributes and helper methods that are provided by
16             # WWW::Spotify (the consuming class), such as _mech(), uri_scheme(),
17             # force_client_auth(), etc.
18             #--------------------------------------------------------------------------
19              
20             # Helper to check if HTTP status code indicates success (2xx range)
21             sub _is_success_status {
22 0     0     my ( $self, $status ) = @_;
23 0   0       return $status >= 200 && $status < 300;
24             }
25              
26             # Helper to URI-encode a value for use in URL paths
27             sub _uri_encode_param {
28 0     0     my ( $self, $value ) = @_;
29 0 0         return defined $value ? uri_escape($value) : '';
30             }
31              
32             sub send_post_request {
33 0     0 0   my $self = shift;
34 0           my $attributes = shift;
35              
36 0           $self->last_error(q{});
37              
38 0           my $url = $self->uri_scheme() . '://' . $self->uri_hostname();
39 0           my $path = $self->_method_to_uri->{ $attributes->{method} };
40              
41 0 0         if ($path) {
42 0           $path
43 0           =~ s/\{([^}]+)\}/$self->_uri_encode_param($attributes->{params}{$1})/ge;
44 0           $url .= $path;
45             }
46              
47 0 0         warn "$url\n" if $self->debug;
48              
49 0           my $mech = $self->_mech;
50              
51 0 0 0       if ( $attributes->{client_auth_required}
52             || $self->force_client_auth() != 0 ) {
53 0 0         if ( $self->current_access_token() eq q{} ) {
54 0 0         warn "Needed to get access token\n" if $self->debug();
55 0           $self->get_client_credentials();
56             }
57             $mech->add_header(
58 0           'Authorization' => 'Bearer ' . $self->current_access_token() );
59             }
60              
61             my $content
62 0 0         = $attributes->{params} ? encode_json( $attributes->{params} ) : '';
63 0           $mech->add_header( 'Content-Type' => 'application/json' );
64 0           $mech->post( $url, Content => $content );
65              
66 0 0         if ( $self->grab_response_header() == 1 ) {
67 0           $self->_set_response_headers($mech);
68             }
69              
70 0           $self->response_status( $mech->status() );
71 0           $self->response_content_type( $mech->content_type() );
72              
73 0 0         if ( $self->_has_custom_request_handler() ) {
74 0           $self->_set_custom_request_handler_result(
75             $self->custom_request_handler()->($mech) );
76             }
77              
78 0 0 0       if ( $self->response_content_type() =~ /application\/json/i
79             && !$self->_is_success_status( $self->response_status() ) ) {
80 0 0         warn "content type is ", $self->response_content_type(), "\n"
81             if $self->debug();
82 0           $self->last_error( "POST request failed, status("
83             . $self->response_status()
84             . ") examine last_result for details" );
85             }
86              
87 0 0 0       die $self->last_error()
88             if $self->die_on_response_error() && $self->last_error ne '';
89              
90 0           return $self->format_results(
91             $mech->content, $mech->ct(),
92             $mech->status()
93             );
94             }
95              
96             sub send_delete_request {
97 0     0 0   my $self = shift;
98 0           my $attributes = shift;
99              
100 0           $self->last_error(q{});
101              
102 0           my $url = $self->uri_scheme() . '://' . $self->uri_hostname();
103 0           my $path = $self->_method_to_uri->{ $attributes->{method} };
104              
105 0 0         if ($path) {
106 0           $path
107 0           =~ s/\{([^}]+)\}/$self->_uri_encode_param($attributes->{params}{$1})/ge;
108 0           $url .= $path;
109             }
110              
111 0 0         warn "$url\n" if $self->debug;
112              
113 0           my $mech = $self->_mech;
114              
115 0 0 0       if ( $attributes->{client_auth_required}
116             || $self->force_client_auth() != 0 ) {
117 0 0         if ( $self->current_access_token() eq q{} ) {
118 0 0         warn "Needed to get access token\n" if $self->debug();
119 0           $self->get_client_credentials();
120             }
121             $mech->add_header(
122 0           'Authorization' => 'Bearer ' . $self->current_access_token() );
123             }
124              
125             my $content
126 0 0         = $attributes->{params} ? encode_json( $attributes->{params} ) : '';
127 0           $mech->add_header( 'Content-Type' => 'application/json' );
128 0           $mech->delete( $url, Content => $content );
129              
130 0 0         if ( $self->grab_response_header() == 1 ) {
131 0           $self->_set_response_headers($mech);
132             }
133              
134 0           $self->response_status( $mech->status() );
135 0           $self->response_content_type( $mech->content_type() );
136              
137 0 0         if ( $self->_has_custom_request_handler() ) {
138 0           $self->_set_custom_request_handler_result(
139             $self->custom_request_handler()->($mech) );
140             }
141              
142 0 0         if ( !$self->_is_success_status( $self->response_status() ) ) {
143 0 0         warn "DELETE request failed with status ", $self->response_status(),
144             "\n" if $self->debug();
145 0           $self->last_error( "DELETE request failed, status("
146             . $self->response_status()
147             . ") examine last_result for details" );
148             }
149              
150 0 0 0       die $self->last_error()
151             if $self->die_on_response_error() && $self->last_error ne '';
152              
153 0           return $self->format_results(
154             $mech->content, $mech->ct(),
155             $mech->status()
156             );
157             }
158              
159             sub send_put_request {
160 0     0 0   my $self = shift;
161 0           my $attributes = shift;
162              
163 0           $self->last_error(q{});
164              
165 0           my $url = $self->uri_scheme() . '://' . $self->uri_hostname();
166 0           my $path = $self->_method_to_uri->{ $attributes->{method} };
167              
168 0 0         if ($path) {
169 0           $path
170 0           =~ s/\{([^}]+)\}/$self->_uri_encode_param($attributes->{params}{$1})/ge;
171 0           $url .= $path;
172             }
173              
174 0 0         warn "$url\n" if $self->debug;
175              
176 0           my $mech = $self->_mech;
177              
178 0 0 0       if ( $attributes->{client_auth_required}
179             || $self->force_client_auth() != 0 ) {
180 0 0         if ( $self->current_access_token() eq q{} ) {
181 0 0         warn "Needed to get access token\n" if $self->debug();
182 0           $self->get_client_credentials();
183             }
184             $mech->add_header(
185 0           'Authorization' => 'Bearer ' . $self->current_access_token() );
186             }
187              
188             my $content
189 0 0         = $attributes->{params} ? encode_json( $attributes->{params} ) : '';
190 0           $mech->add_header( 'Content-Type' => 'application/json' );
191 0           $mech->put( $url, Content => $content );
192              
193 0 0         if ( $self->grab_response_header() == 1 ) {
194 0           $self->_set_response_headers($mech);
195             }
196              
197 0           $self->response_status( $mech->status() );
198 0           $self->response_content_type( $mech->content_type() );
199              
200 0 0         if ( $self->_has_custom_request_handler() ) {
201 0           $self->_set_custom_request_handler_result(
202             $self->custom_request_handler()->($mech) );
203             }
204              
205 0 0         if ( !$self->_is_success_status( $self->response_status() ) ) {
206 0 0         warn "PUT request failed with status ", $self->response_status(), "\n"
207             if $self->debug();
208 0           $self->last_error( "PUT request failed, status("
209             . $self->response_status()
210             . ") examine last_result for details" );
211             }
212              
213 0 0 0       die $self->last_error()
214             if $self->die_on_response_error() && $self->last_error ne '';
215              
216 0           return $self->format_results(
217             $mech->content, $mech->ct(),
218             $mech->status()
219             );
220             }
221              
222             sub send_get_request {
223 0     0 0   my $self = shift;
224 0           my $attributes = shift;
225              
226 0           my $uri_params = q{};
227 0 0         warn "attributes: "
    0          
228             . ( defined $attributes ? 'defined' : 'undef' ) . "\n"
229             if $self->debug();
230              
231 0           $self->last_error(q{});
232              
233 0 0 0       if ( defined $attributes->{extras}
234             && ref $attributes->{extras} eq 'HASH' ) {
235 0           my @tmp;
236 0           foreach my $key ( keys %{ $attributes->{extras} } ) {
  0            
237 0           push @tmp, "$key=$attributes->{extras}{$key}";
238             }
239 0           $uri_params = join( '&', @tmp );
240 0 0         warn "uri_params: $uri_params\n" if $self->debug();
241             }
242              
243 0 0 0       if ( exists $attributes->{format}
244             && $attributes->{format} =~ /json|jsonp/ ) {
245 0           $self->result_format( $attributes->{format} );
246 0           delete $attributes->{format};
247             }
248              
249 0           my $url;
250 0 0         if ( $attributes->{method} eq 'query_full_url' ) {
251 0           $url = $attributes->{url};
252             }
253             else {
254 0           $url = $self->uri_scheme() . '://' . $self->uri_hostname();
255              
256 0           my $path = $self->_method_to_uri->{ $attributes->{method} };
257 0 0         if ($path) {
258 0 0         warn "raw: $path" if $self->debug();
259              
260 0 0 0       if ( $path =~ /search/ && $attributes->{method} eq 'search' ) {
    0 0        
    0 0        
261 0           my $q = $self->_uri_encode_param( $attributes->{q} );
262 0           my $type = $self->_uri_encode_param( $attributes->{type} );
263 0           $path =~ s/\{q\}/$q/;
264 0           $path =~ s/\{type\}/$type/;
265             }
266             elsif ( $path =~ m/\{id\}/ && exists $attributes->{params}{id} ) {
267             my $id
268 0           = $self->_uri_encode_param( $attributes->{params}{id} );
269 0           $path =~ s/\{id\}/$id/;
270             }
271             elsif ( $path =~ m/\{ids\}/ && exists $attributes->{params}{ids} )
272             {
273             my $ids
274 0           = $self->_uri_encode_param( $attributes->{params}{ids} );
275 0           $path =~ s/\{ids\}/$ids/;
276             }
277              
278 0 0         if ( $path =~ m/\{country\}/ ) {
279             my $country = $self->_uri_encode_param(
280 0           $attributes->{params}{country} );
281 0           $path =~ s/\{country\}/$country/;
282             }
283              
284 0 0 0       if ( $path =~ m/\{user_id\}/
285             && exists $attributes->{params}{user_id} ) {
286             my $user_id = $self->_uri_encode_param(
287 0           $attributes->{params}{user_id} );
288 0           $path =~ s/\{user_id\}/$user_id/;
289             }
290              
291 0 0 0       if ( $path =~ m/\{playlist_id\}/
292             && exists $attributes->{params}{playlist_id} ) {
293             my $playlist_id = $self->_uri_encode_param(
294 0           $attributes->{params}{playlist_id} );
295 0           $path =~ s/\{playlist_id\}/$playlist_id/;
296             }
297              
298 0 0 0       if ( $path =~ m/\{category_id\}/
299             && exists $attributes->{params}{category_id} ) {
300             my $category_id = $self->_uri_encode_param(
301 0           $attributes->{params}{category_id} );
302 0           $path =~ s/\{category_id\}/$category_id/;
303             }
304              
305 0 0         warn "modified: $path\n" if $self->debug();
306 0           $url .= $path;
307             }
308             }
309              
310 0 0         if ($uri_params) {
311 0 0         my $start_with = $url =~ /\?/ ? '&' : '?';
312 0           $url .= $start_with . $uri_params;
313             }
314              
315 0 0         warn "$url\n" if $self->debug;
316              
317 0           my $mech = $self->_mech;
318              
319 0 0 0       if ( $attributes->{client_auth_required}
320             || $self->force_client_auth() != 0 ) {
321 0 0         if ( $self->current_access_token() eq q{} ) {
322 0 0         warn "Needed to get access token\n" if $self->debug();
323 0           $self->get_client_credentials();
324             }
325             $mech->add_header(
326 0           'Authorization' => 'Bearer ' . $self->current_access_token() );
327             }
328              
329 0           $mech->get($url);
330              
331 0 0         if ( $self->grab_response_header() == 1 ) {
332 0           $self->_set_response_headers($mech);
333             }
334              
335 0           $self->response_status( $mech->status() );
336 0           $self->response_content_type( $mech->content_type() );
337              
338 0 0         if ( $self->_has_custom_request_handler() ) {
339 0           $self->_set_custom_request_handler_result(
340             $self->custom_request_handler()->($mech) );
341             }
342              
343 0 0 0       if ( $self->response_content_type() =~ /application\/json/i
344             && !$self->_is_success_status( $self->response_status() ) ) {
345 0 0         warn "content type is " . $self->response_content_type() . "\n"
346             if $self->debug();
347 0           $self->last_error( "GET request failed, status("
348             . $self->response_status()
349             . ") examine last_result for details" );
350             }
351              
352 0 0 0       die $self->last_error()
353             if $self->die_on_response_error() && $self->last_error ne '';
354              
355 0           return $self->format_results(
356             $mech->content, $mech->ct(),
357             $mech->status()
358             );
359             }
360              
361             # --------------------------------------------------------------
362             # Convenience wrapper so callers may use a full URL directly.
363             # --------------------------------------------------------------
364              
365             sub query_full_url {
366 0     0 0   my $self = shift;
367 0           my $url = shift;
368 0   0       my $client_auth_required = shift || 0;
369              
370 0           return $self->send_get_request(
371             {
372             method => 'query_full_url',
373             url => $url,
374             client_auth_required => $client_auth_required,
375             }
376             );
377             }
378              
379             # A tiny helper that gives us access to the private %method_to_uri mapping
380             # stored in WWW::Spotify. We expose it via a method so the hashref can be
381             # reused rather than duplicated.
382              
383             sub _method_to_uri {
384 4     4   43 no strict 'refs'; ## no critic
  4         8  
  4         373  
385 0     0     return \%{ 'WWW::Spotify::' . 'method_to_uri' };
  0            
386             }
387              
388             1;
389              
390             # ABSTRACT: Generic HTTP helpers extracted into a role
391              
392             # vim: ts=4 sts=4 sw=4 et
393              
394             __END__