File Coverage

blib/lib/IO/Iron/Connector.pm
Criterion Covered Total %
statement 68 219 31.0
branch 0 54 0.0
condition 0 24 0.0
subroutine 19 24 79.1
pod 3 3 100.0
total 90 324 27.7


line stmt bran cond sub pod time code
1             package IO::Iron::Connector;
2              
3             ## no critic (Documentation::RequirePodAtEnd)
4             ## no critic (Documentation::RequirePodSections)
5             ## no critic (RegularExpressions::RequireExtendedFormatting)
6             ## no critic (RegularExpressions::RequireLineBoundaryMatching)
7             ## no critic (RegularExpressions::ProhibitEscapedMetacharacters)
8              
9 3     3   58 use 5.010_000;
  3         11  
10 3     3   20 use strict;
  3         5  
  3         62  
11 3     3   14 use warnings;
  3         6  
  3         131  
12              
13             # Global creator
14 3         256 BEGIN {
15 3     3   19 use parent qw( IO::Iron::ConnectorBase ); # Inheritance
  3         6  
  3         37  
16             }
17              
18             # Global destructor
19       3     END {
20             }
21              
22              
23             # ABSTRACT: REST API Connector, HTTP interface class.
24              
25             our $VERSION = '0.13'; # VERSION: generated by DZP::OurPkgVersion
26              
27              
28              
29 3     3   6 use Log::Any qw{$log};
  3         16  
  3         1035  
30             require JSON::MaybeXS;
31 3     3   607 use Data::UUID ();
  3         81  
  3         16  
32 3     3   45 use Hash::Util 0.06 qw{lock_keys lock_keys_plus unlock_keys legal_keys};
  3         18  
  3         211  
33 3     3   21 use Carp::Assert;
  3         17  
  3         359  
34 3     3   7 use Carp::Assert::More;
  3         462  
  3         21  
35 3     3   9 use Carp;
  3         151  
  3         23  
36 3     3   6 use English '-no_match_vars';
  3         20  
  3         2602  
37 3     3   140393 use REST::Client ();
  3         99  
  3         27  
38 3     3   7 use URI::Escape qw{uri_escape_utf8};
  3         181  
  3         21  
39 3     3   9 use Try::Tiny;
  3         160  
  3         22  
40 3     3   7 use Scalar::Util qw{blessed looks_like_number};
  3         166  
  3         511  
41             use Exception::Class (
42 3         1156 'IronHTTPCallException' => {
43             fields => ['status_code', 'response_message'],
44             }
45 3     3   8534 );
  3         28  
46              
47             # CONSTANTS
48              
49 3     3   9 use Const::Fast;
  3         28  
  3         8  
50             const my $HTTP_CODE_OK_MIN => 200;
51             const my $HTTP_CODE_OK_MAX => 299;
52             const my $HTTP_CODE_SERVICE_UNAVAILABLE => 503;
53             const my $HTTP_CONTENT_TYPE_JSON => q{application/json; charset=utf-8};
54              
55              
56             sub new {
57 3     3 1 35 my ($class) = @_;
58 3         280 $log->tracef('Entering new(%s)', $class);
59 3         7 my $self = IO::Iron::ConnectorBase->new();
60             # Add more keys to the self hash.
61             my @self_keys = (
62             'client', # REST client timeout (for REST calls accessing Iron services).
63             'mime_boundary', # The boundary string separating parts in multipart REST messages.
64 3         13 legal_keys(%{$self}),
  3         22  
65             );
66 3         13 unlock_keys(%{$self});
  3         29  
67 3         6 bless $self, $class;
68 3         19 lock_keys(%{$self}, @self_keys);
  3         137  
69              
70             # Set up REST client
71 3         15534 my $client = REST::Client->new();
72 3         796 $self->{'client'} = $client;
73              
74             # Create MIME multipart message boundary string
75 3         540 my $ug = Data::UUID->new();
76 3         49 my $uuid1 = $ug->create();
77 3         33 $self->{'mime_boundary'} = 'MIME_BOUNDARY_' . (substr $ug->to_string($uuid1), 1, 20); ## no critic (ValuesAndExpressions::ProhibitMagicNumbers)
78              
79 3         191 $log->infof('Iron Connector created with REST::Client as HTTP user agent.');
80 3         1821 $log->tracef('Exiting new: %s', $self);
81 0           return $self;
82             }
83              
84              
85             # TODO check why previous message (coded content) is in the next message!
86             sub perform_iron_action { ## no critic (Subroutines::ProhibitExcessComplexity)
87 0     0 1   my ($self, $iron_action, $params) = @_;
88 0 0         if(!defined $params) {
89 0           $params = {};
90             }
91 0           $log->tracef('Entering Connector:perform_iron_action(%s, %s)', $iron_action, $params);
92              
93 0           my $href = $iron_action->{'href'};
94 0           my $action_verb = $iron_action->{'action'};
95 0           my $return_type = $iron_action->{'return'};
96 0           my $retry = $iron_action->{'retry'};
97 0           my $require_body = $iron_action->{'require_body'};
98 0 0         my $paged = $iron_action->{'paged'} ? $iron_action->{'paged'} : 0;
99 0 0         my $per_page = $iron_action->{'per_page'} ? $iron_action->{'per_page'} : 0;
100 0           my $url_params = q{};
101 0 0 0       if(exists $iron_action->{'url_params'} && ref $iron_action->{'url_params'} eq 'HASH') {
102 0           foreach (keys %{$iron_action->{'url_params'}}) {
  0            
103 0           $log->tracef('perform_iron_action(): url_param:%s', $_);
104 0 0         if ($params->{'{'.$_.'}'}) {
105 0           $url_params .= "$_={$_}&";
106             }
107             }
108 0           $url_params = substr $url_params, 0, (length $url_params) - 1;
109             }
110 0 0         if ($url_params) {
111 0           $href .= (q{?} . $url_params);
112             }
113 0           my $content_type = $iron_action->{'content_type'};
114 0           $params->{'content_type'} = $content_type;
115 0           $params->{'return_type'} = $return_type;
116 0           $log->tracef('href before value substitution:\'%s\'.', $href);
117 0           foreach my $value_key (sort keys %{$params}) {
  0            
118 0           my $value = $params->{$value_key};
119 0           $log->tracef('Param key:%s; value=%s;', $value_key, $value);
120 0           $href =~ s/$value_key/$value/gs;
121             };
122 0           $log->tracef('href after value substitution:\'%s\'.', $href);
123              
124 0           my ($http_status_code, $returned_msg);
125 0           my $keep_on_trying = 1;
126 0           while($keep_on_trying) {
127 0           $keep_on_trying = 0;
128             try {
129             assert(
130             ($require_body == 1 && defined $params->{'body'} && ref $params->{'body'} eq 'HASH')
131 0   0 0     || ($require_body == 0 && !defined $params->{'body'})
132             );
133 0           assert_in($action_verb, ['GET','PATCH','PUT','POST','DELETE','OPTIONS','HEAD'], 'action_verb is a valid HTTP verb.');
134 0           assert_nonblank( $params->{'{Protocol}'}, 'params->{Protocol} is defined and not blank.' );
135 0           assert_nonblank( $params->{'{Port}'}, 'params->{Port} is defined and not blank.' );
136 0           assert_nonblank( $params->{'{Host}'}, 'params->{Host} is defined and not blank.' );
137 0           assert_nonblank( $params->{'{Project ID}'}, 'params->{Project ID} is defined and not blank.' );
138 0           assert_nonblank( $params->{'{API Version}'}, 'params->{API Version} is defined and not blank.' );
139 0           assert_nonblank( $params->{'authorization_token'}, 'params->{authorization_token} is defined and not blank.' );
140 0           assert_nonblank( $params->{'http_client_timeout'}, 'params->{http_client_timeout} is defined and not blank.' );
141 0 0         my $url_escape_these_fields = defined $iron_action->{'url_escape'} ? $iron_action->{'url_escape'} : {};
142 0           foreach my $field_name (keys %{$url_escape_these_fields}) {
  0            
143 0 0         if (defined $params->{$field_name}) {
144 0           $params->{$field_name} = uri_escape_utf8($params->{$field_name});
145             }
146             }
147             #
148 0 0         if($paged) {
149 0           $log->debugf('A paged query.');
150 0           my @returned_msgs;
151 0           my ($http_status_code_temp, $returned_msg_temp);
152 0           my $page_number = 0;
153 0           while(1) {
154 0           my $page_href = $href;
155 0           $log->debugf('A paged query. Href:\'%s\'', $page_href);
156 0 0         $page_href .= ($href =~ /\?/gsx ? q{&} : q{?}) . 'per_page='.$per_page.'&page='.$page_number;
157 0           ($http_status_code_temp, $returned_msg_temp) =
158             $self->perform_http_action($action_verb, $page_href, $params);
159 0           my $return_list = $returned_msg_temp;
160 0           my ($return_type_def, $list_hash_key) = (split m/:/s, $return_type);
161 0 0 0       $return_list = $returned_msg_temp->{$list_hash_key}
162             if $return_type_def eq 'LIST' && defined $list_hash_key; ## no critic (ControlStructures::ProhibitPostfixControls)
163 0           push @returned_msgs, @{$return_list};
  0            
164 0 0 0       if( scalar @{$return_list} == 0 || @{$return_list} < $per_page ) {
  0            
  0            
165 0           last;
166             }
167 0           $page_number++;
168             }
169 0           $http_status_code = $http_status_code_temp;
170 0           $returned_msg = \@returned_msgs;
171             }
172             else {
173 0           ($http_status_code, $returned_msg) = $self->perform_http_action($action_verb, $href, $params);
174             }
175             }
176             catch {
177 0     0     $log->debugf('perform_iron_action(): Caught exception:\'%s\'.', $_);
178 0 0 0       croak $_ unless blessed $_ && $_->can('rethrow'); ## no critic (ControlStructures::ProhibitPostfixControls)
179 0 0         if ( $_->isa('IronHTTPCallException') ) {
180 0 0         if( $_->status_code == $HTTP_CODE_SERVICE_UNAVAILABLE ) {
181             # 503 Service Unavailable. Clients should implement exponential backoff to retry the request.
182 0 0         $keep_on_trying = 1 if ($retry == 1); ## no critic (ControlStructures::ProhibitPostfixControls)
183             # TODO Fix this temporary solution for backoff to retry the request.
184             }
185             else {
186 0           $log->debugf('perform_iron_action(): rethrow the exception.', $_);
187 0           $_->rethrow;
188             }
189             }
190             else {
191 0           $_->rethrow;
192             }
193 0           };
194             # Module::Pluggable here?
195             }
196 0           $log->tracef('Exiting Connector:perform_iron_action(): %s', $returned_msg );
197 0           return $http_status_code, $returned_msg;
198             }
199              
200              
201              
202             sub perform_http_action {
203 0     0 1   my ($self, $action_verb, $href, $params) = @_;
204 0           my $client = $self->{'client'};
205 0           my $json = JSON::MaybeXS->new(utf8 => 1, pretty => 1);
206             # TODO assert href is URL
207 0           assert_in($action_verb, ['GET','PATCH','PUT','POST','DELETE','OPTIONS','HEAD'], 'action_verb is a valid HTTP verb.');
208 0           assert_exists($params, ['http_client_timeout', 'authorization_token'], 'params contains items http_client_timeout and authorization_token.');
209 0           assert_integer($params->{'http_client_timeout'}, 'params->{\'http_client_timeout\'} is integer.');
210 0           assert_nonblank($params->{'authorization_token'}, 'params->{\'authorization_token\'} is a non-blank string.');
211 0           $log->tracef('Entering Connector:perform_http_action(%s, %s, %s)', $action_verb, $href, $params);
212             #
213             # HTTP request attributes
214 0           my $timeout = $params->{'http_client_timeout'};
215 0           my $request_body;
216             # Headers
217 0 0         my $content_type = defined($params->{'content_type'}) ? $params->{'content_type'} : $HTTP_CONTENT_TYPE_JSON;
218 0           my $authorization = 'OAuth ' . $params->{'authorization_token'};
219             #
220 0 0         if($content_type =~ /multipart/is) {
221 0 0         my $body_content = $params->{'body'} ? $params->{'body'} : { }; # Else use an empty hash for body.
222 0           my $file_as_zip = $params->{'body'}->{'file'};
223 0           delete $params->{'body'}->{'file'};
224 0           my $encoded_body_content = $json->encode($body_content);
225 0           my $boundary = $self->{'mime_boundary'};
226 0           $content_type = "multipart/form-data; boundary=$boundary";
227 0           my $file_name = $params->{'body'}->{'file_name'} . '.zip';
228             #$request_body = 'MIME-Version: 1.0' . "\n";
229             #$request_body .= 'Content-Length: ' . $req_content_length . "\n";
230             #$request_body .= 'Content-Type: ' . $req_content_type . "\n";
231 0           $request_body = q{--} . $boundary . "\n";
232 0           $request_body .= 'Content-Disposition: ' . 'form-data; name="data"' . "\n";
233 0           $request_body .= 'Content-Type: ' . 'text/plain; charset=utf-8' . "\n";
234 0           $request_body .= "\n";
235 0           $request_body .= $encoded_body_content . "\n";
236 0           $request_body .= "\n";
237 0           $request_body .= q{--} . $boundary . "\n";
238 0           $request_body .= 'Content-Disposition: ' . 'form-data; name="file"; filename="' . $file_name . q{"} . "\n";
239 0           $request_body .= 'Content-Type: ' . 'application/zip' . "\n";
240 0           $request_body .= 'Content-Transfer-Encoding: base64' . "\n";
241 0           $request_body .= "\n";
242 0           $request_body .= $file_as_zip . "\n";
243 0           $request_body .= q{--} . $boundary . q{--} . "\n";
244             }
245             else {
246 0 0         my $body_content = $params->{'body'} ? $params->{'body'} : { }; # Else use an empty hash for body.
247 0           $log->debugf('About to jsonize the body:\'%s\'', $body_content);
248 0           foreach (keys %{$body_content}) {
  0            
249             # Gimmick to ensure the proper jsonization of numbers
250             # Otherwise numbers might end up as strings.
251 0 0         $body_content->{$_} += 0 if looks_like_number $body_content->{$_}; ## no critic (ControlStructures::ProhibitPostfixControls)
252             }
253 0           my $encoded_body_content = $json->encode($body_content);
254 0           $log->debugf('Jsonized body:\'%s\'', $encoded_body_content);
255 0           $request_body = $encoded_body_content;
256             }
257 0           $client->setTimeout($timeout);
258 0           $log->tracef('client: %s; action=%s; href=%s;', $client, $action_verb, $href);
259 0           $log->debugf('REST Request: [verb=%s; href=%s; body=%s; Headers: Content-Type=%s; Authorization=%s]', $action_verb, $href, $request_body, $content_type, $authorization);
260 0           $client->request($action_verb, $href, $request_body,
261             {
262             'Content-Type' => $content_type,
263             'Authorization' => $authorization,
264             });
265             # RETURN:
266 0           $log->debugf('Returned HTTP response code:%s', $client->responseCode());
267 0           $log->tracef('Returned HTTP response:%s', $client->responseContent());
268 0 0 0       if( $client->responseCode() >= $HTTP_CODE_OK_MIN && $client->responseCode() <= $HTTP_CODE_OK_MAX ) {
269             # 200 OK: Successful GET; 201 Created: Successful POST
270 0           $log->tracef('HTTP Response code: %d, %s', $client->responseCode(), 'Successful!');
271 0           my $decoded_body_content;
272 0 0 0       if(defined $params->{'return_type'} && $params->{'return_type'} eq 'BINARY') {
    0 0        
273 0           $log->tracef('Returned HTTP response header Content-Disposition:%s', $client->responseHeader('Content-Disposition'));
274 0           my $filename;
275 0 0         if($client->responseHeader ('Content-Disposition') =~ /filename=(.+)$/s) {
276 0 0         $filename = $1 ? $1 : '[Unknown filename]';
277             }
278 0           $decoded_body_content = { 'file' => $client->responseContent(), 'file_name' => $filename };
279             }
280             elsif(defined $params->{'return_type'} && $params->{'return_type'} eq 'PLAIN_TEXT') {
281 0           $decoded_body_content = $client->responseContent();
282             }
283             else {
284 0           $decoded_body_content = $json->decode( $client->responseContent() );
285             }
286 0           $log->tracef('Exiting Connector:perform_http_action(): %s, %s', $client->responseCode(), $decoded_body_content );
287 0           return $client->responseCode(), $decoded_body_content;
288             }
289             else {
290 0           $log->tracef('HTTP Response code: %d, %s', $client->responseCode(), 'Failure!');
291 0           my $decoded_body_content;
292             try {
293 0     0     $decoded_body_content = $json->decode( $client->responseContent() );
294 0           };
295 0 0         my $response_message = $decoded_body_content ? $decoded_body_content->{'msg'} : $client->responseContent();
296 0           $log->tracef('Throwing exception in perform_http_action(): status_code=%s, response_message=%s', $client->responseCode(), $response_message );
297 0           IronHTTPCallException->throw(
298             status_code => $client->responseCode(),
299             response_message => $response_message,
300             error => 'IronHTTPCallException: status_code=' . $client->responseCode()
301             . ' response_message=' . $response_message,
302             );
303             }
304             return; # Control does not reach this point.
305             }
306              
307             1;
308              
309             __END__