File Coverage

blib/lib/Nexus/REST.pm
Criterion Covered Total %
statement 32 128 25.0
branch 0 60 0.0
condition 0 14 0.0
subroutine 11 22 50.0
pod 5 5 100.0
total 48 229 20.9


line stmt bran cond sub pod time code
1             package Nexus::REST;
2             # ABSTRACT: Thin wrapper around Nexus's REST API
3             $Nexus::REST::VERSION = '0.002';
4 1     1   65718 use 5.010;
  1         2  
5 1     1   459 use utf8;
  1         12  
  1         4  
6 1     1   24 use strict;
  1         2  
  1         15  
7 1     1   3 use warnings;
  1         2  
  1         19  
8              
9 1     1   3 use Carp;
  1         1  
  1         49  
10 1     1   417 use URI;
  1         5263  
  1         30  
11 1     1   367 use MIME::Base64;
  1         501  
  1         53  
12 1     1   6 use URI::Escape;
  1         2  
  1         41  
13 1     1   496 use JSON 2.23;
  1         7766  
  1         4  
14 1     1   470 use Data::Util qw/:check/;
  1         515  
  1         130  
15 1     1   359 use REST::Client;
  1         34377  
  1         1154  
16              
17             sub new {
18 0     0 1   my ($class, $URL, $username, $password, $rest_client_config) = @_;
19              
20 0 0         $URL = URI->new($URL) if is_string($URL);
21 0 0         is_instance($URL, 'URI')
22             or croak __PACKAGE__ . "::new: URL argument must be a string or a URI object.\n";
23              
24             # Append the service prefix if not already specified
25 0 0         if ($URL->path =~ m@^/*$@) {
26 0           $URL->path($URL->path . '/service');
27             }
28              
29             # If no password is set we try to lookup the credentials in the .netrc file
30 0 0         if (! defined $password) {
31 0 0         eval {require Net::Netrc}
  0            
32             or croak "Can't require Net::Netrc module. Please, specify the USERNAME and PASSWORD.\n";
33 0 0         if (my $machine = Net::Netrc->lookup($URL->host, $username)) { # $username may be undef
34 0           $username = $machine->login;
35 0           $password = $machine->password;
36             } else {
37 0           croak "No credentials found in the .netrc file.\n";
38             }
39             }
40              
41 0 0         is_string($username)
42             or croak __PACKAGE__ . "::new: USERNAME argument must be a string.\n";
43              
44 0 0         is_string($password)
45             or croak __PACKAGE__ . "::new: PASSWORD argument must be a string.\n";
46              
47 0 0         $rest_client_config = {} unless defined $rest_client_config;
48 0 0         is_hash_ref($rest_client_config)
49             or croak __PACKAGE__ . "::new: REST_CLIENT_CONFIG argument must be a hash-ref.\n";
50              
51 0           my $rest = REST::Client->new($rest_client_config);
52              
53             # Set default base URL
54 0           $rest->setHost($URL);
55              
56             # Follow redirects/authentication by default
57 0           $rest->setFollow(1);
58              
59             # Since Nexus doesn't send an authentication chalenge, we may
60             # simply force the sending of the authentication header.
61 0           $rest->addHeader(Authorization => 'Basic ' . encode_base64("$username:$password"));
62              
63 0           $rest->addHeader(Accept => 'application/json');
64              
65             # Configure UserAgent name
66 0           $rest->getUseragent->agent(__PACKAGE__);
67              
68 0           return bless {
69             rest => $rest,
70             json => JSON->new->utf8->allow_nonref,
71             } => $class;
72             }
73              
74             sub _error {
75 0     0     my ($self, $content, $type, $code) = @_;
76              
77 0 0         $type = 'text/plain' unless $type;
78 0 0         $code = 500 unless $code;
79              
80 0           my $msg = __PACKAGE__ . " Error[$code";
81              
82 0 0         if (eval {require HTTP::Status}) {
  0            
83 0 0         if (my $status = HTTP::Status::status_message($code)) {
84 0           $msg .= " - $status";
85             }
86             }
87              
88 0           $msg .= "]:\n";
89              
90 0 0 0       if ($type =~ m:text/plain:i) {
    0          
    0          
91 0           $msg .= $content;
92             } elsif ($type =~ m:application/json:) {
93 0           my $error = $self->{json}->decode($content);
94 0 0 0       if (ref $error eq 'HASH' && exists $error->{errorMessages}) {
95 0           foreach my $message (@{$error->{errorMessages}}) {
  0            
96 0           $msg .= "- $message\n";
97             }
98             } else {
99 0           $msg .= $content;
100             }
101 0           } elsif ($type =~ m:text/html:i && eval {require HTML::TreeBuilder}) {
102 0           $msg .= HTML::TreeBuilder->new_from_content($content)->as_text;
103             } else {
104 0           $msg .= "";
105             };
106 0           $msg =~ s/\n*$/\n/s; # end message with a single newline
107 0           return $msg;
108             }
109              
110             sub _content {
111 0     0     my ($self) = @_;
112              
113 0           my $rest = $self->{rest};
114 0           my $code = $rest->responseCode();
115 0           my $type = $rest->responseHeader('Content-Type');
116 0           my $content = $rest->responseContent();
117              
118 0 0         $code =~ /^2/
119             or croak $self->_error($content, $type, $code);
120              
121 0 0         return unless $content;
122              
123 0 0         if (! defined $type) {
    0          
    0          
124 0           croak $self->_error("Cannot convert response content with no Content-Type specified.");
125             } elsif ($type =~ m:^application/json:i) {
126 0           return $self->{json}->decode($content);
127             } elsif ($type =~ m:^text/plain:i) {
128 0           return $content;
129             } else {
130 0           croak $self->_error("I don't understand content with Content-Type '$type'.");
131             }
132             }
133              
134             sub _build_query {
135 0     0     my ($self, $query) = @_;
136              
137 0 0         is_hash_ref($query) or croak $self->_error("The QUERY argument must be a hash-ref.");
138              
139 0           return '?'. join('&', map {$_ . '=' . uri_escape($query->{$_})} keys %$query);
  0            
140             }
141              
142             sub GET {
143 0     0 1   my ($self, $path, $query) = @_;
144              
145 0 0         $path .= $self->_build_query($query) if $query;
146              
147 0           $self->{rest}->GET($path);
148              
149 0           return $self->_content();
150             }
151              
152             sub DELETE {
153 0     0     my ($self, $path, $query) = @_;
154              
155 0 0         $path .= $self->_build_query($query) if $query;
156              
157 0           $self->{rest}->DELETE($path);
158              
159 0           return $self->_content();
160             }
161              
162             sub PUT {
163 0     0 1   my ($self, $path, $query, $value, $headers) = @_;
164              
165 0 0         $path .= $self->_build_query($query) if $query;
166              
167 0   0       $headers //= {};
168 0   0       $headers->{'Content-Type'} //= 'application/json;charset=UTF-8';
169              
170 0           $self->{rest}->PUT($path, $self->{json}->encode($value), $headers);
171              
172 0           return $self->_content();
173             }
174              
175             sub POST {
176 0     0 1   my ($self, $path, $query, $value, $headers) = @_;
177              
178 0 0         $path .= $self->_build_query($query) if $query;
179              
180 0   0       $headers //= {};
181 0   0       $headers->{'Content-Type'} //= 'application/json;charset=UTF-8';
182              
183 0           $self->{rest}->POST($path, $self->{json}->encode($value), $headers);
184              
185 0           return $self->_content();
186             }
187              
188             sub get_iterator {
189 0     0 1   my ($self, $path, $query) = @_;
190              
191 0           return Nexus::REST::Iterator->new($self, $path, $query);
192             }
193              
194             package Nexus::REST::Iterator;
195             $Nexus::REST::Iterator::VERSION = '0.002';
196             sub new {
197 0     0     my ($class, $nexus, $path, $query) = @_;
198              
199 0           return bless {
200             nexus => $nexus,
201             path => $path,
202             query => $query,
203             batch => $nexus->GET($path, $query),
204             } => $class;
205             }
206              
207             sub next {
208 0     0     my ($self) = @_;
209              
210 0 0         unless (@{$self->{batch}{items}}) {
  0            
211 0 0         unless ($self->{batch}{continuationToken}) {
212 0           delete @{$self}{keys %$self}; # Don't hold a reference to Nexus more than needed
  0            
213 0           return;
214             }
215 0           $self->{query}{continuationToken} = $self->{batch}{continuationToken};
216 0           $self->{batch} = $self->{nexus}->GET($self->{path}, $self->{query});
217             }
218              
219 0           return shift @{$self->{batch}{items}};
  0            
220             }
221              
222             1;
223              
224             __END__