File Coverage

blib/lib/JSON/API.pm
Criterion Covered Total %
statement 129 139 92.8
branch 40 54 74.0
condition 18 25 72.0
subroutine 24 26 92.3
pod 11 11 100.0
total 222 255 87.0


line stmt bran cond sub pod time code
1             package JSON::API;
2 4     4   658824 use strict;
  4         5  
  4         170  
3 4     4   1663 use HTTP::Status qw/:constants/;
  4         13137  
  4         1615  
4 4     4   2798 use LWP::UserAgent;
  4         159007  
  4         150  
5 4     4   2384 use JSON;
  4         33677  
  4         42  
6 4     4   2395 use Data::Dumper;
  4         25192  
  4         273  
7 4     4   1574 use URI::Encode qw/uri_encode/;
  4         39057  
  4         248  
8              
9             BEGIN {
10 4     4   27 use Exporter ();
  4         7  
  4         104  
11 4     4   20 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  4         6  
  4         309  
12 4     4   10 $VERSION = v1.2.0;
13 4         47 @ISA = qw(Exporter);
14             #Give a hoot don't pollute, do not export more than needed by default
15 4         6 @EXPORT = qw();
16 4         6 @EXPORT_OK = qw();
17 4         6250 %EXPORT_TAGS = ();
18             }
19              
20             # not an object metohd
21             # build JSON object for encode/decode, disallow nonref
22             # to match previous non object usage, APIs should be returning
23             # JSON arrays or JSON objects
24             sub _build_json {
25             # utf8: encode/decode operates on byte strings. HTTP::Request requires
26             # bytes; without this, non-ASCII payloads carry the utf8 flag and trip
27             # "HTTP::Message content must be bytes".
28 8     8   153 JSON->new->utf8->allow_nonref(0);
29             }
30             sub _debug
31             {
32 193     193   9714 my ($self, @lines) = @_;
33 193         539 my $output = join('\n', @lines);
34 193 100       671 print STDERR $output . "\n" if ($self->{debug});
35             }
36              
37             sub _server
38             {
39 45     45   91 my ($self, $input) = @_;
40 45         225 $input =~ s|^(https?://)?||;
41 45         227 $input =~ m|^([^\s:/]+)(:\d+)?.*|;
42 45   100     210 $input = $1 . ($2 || '');
43 45         113 return $input;
44             }
45              
46             sub _http_req
47             {
48 26     26   77 my ($self, $method, $path, $data, $apphdr) = @_;
49 26         112 $self->_debug('_http_req called with the following:',Dumper($method,$path,$data, $apphdr));
50              
51 26         110 my $url = $self->url($path);
52 26         74 $self->_debug("URL calculated to be: $url");
53 26         254 delete $self->{response};
54              
55 26         144 my $headers = HTTP::Headers->new(
56             'Accept' => 'application/json',
57             'Content-Type' => 'application/json',
58             );
59 26 50 33     1912 if( $apphdr && ref $apphdr ) {
60 0         0 $headers->header( $_, $apphdr->{$_} ) foreach (keys %$apphdr);
61             }
62 26         37 my $json;
63 26 100       48 if (defined $data) {
64 8         20 $json = $self->_encode($data);
65 8 100       45 return (wantarray ? (500, {}) : {}) unless defined $json;
    100          
66             }
67              
68 24         205 my $req = HTTP::Request->new($method, $url, $headers, $json);
69 24         3676 $self->_debug("Requesting: ",Dumper($req));
70 24         160 my $res = $self->{user_agent}->request($req);
71              
72 24         220373 $self->_debug("Response: ",Dumper($res));
73 24         82 $self->{response} = $res;
74 24 100       84 if ($res->is_success) {
    50          
75 18         233 $self->{has_error} = 0;
76 18         48 $self->{error_string} = '';
77 18         41 $self->_debug("Successful request detected");
78             } elsif ($res->code == HTTP_NOT_MODIFIED) {
79             return wantarray ?
80 0 0       0 ($res->code, {}) :
81             {};
82             } else {
83 6         102 $self->{has_error} = 1;
84 6         13 $self->{error_string} = $res->content;
85 6         67 $self->_debug("Error detected: ".$self->{error_string});
86             # If internal warning, return before decoding, as it will fail + overwrite the error_string
87 6 50       11 if ($res->header('client-warning') =~ m/internal response/i) {
88 0 0       0 return wantarray ? ($res->code, {}) : {};
89             }
90             }
91 24 100 100     494 my $decoded = $res->content ? ($self->_decode($res->content) || {}) : {};
92              
93             #FIXME: should we auto-populate an error key in the {} if error detected but no content?
94             return wantarray ?
95 24 100       145 ($res->code, $decoded) :
96             $decoded;
97             }
98              
99             sub _encode
100             {
101 13     13   56 my ($self, $obj) = @_;
102              
103             # Validate input ourselves rather than relying on JSON backend's
104             # allow_nonref(0). Modern JSON::PP/JSON::XS default allow_nonref to
105             # true and may ignore allow_nonref(0); error message text also varies
106             # across backends. Guarantee consistent behavior + errstr here.
107 13 100 66     50 unless (ref($obj) eq 'HASH' || ref($obj) eq 'ARRAY') {
108 3         7 $self->{has_error} = 1;
109 3         6 $self->{error_string} = 'hash- or arrayref expected (not a simple scalar, use allow_nonref to allow this)';
110 3         17 $self->_debug("Error serializing json from \$obj:" . $self->{error_string});
111 3         10 return undef; ## no critic (ProhibitExplicitReturnUndef)
112             }
113              
114 10         17 my $json = undef;
115             eval {
116 10   66     37 $self->{_json} ||= _build_json();
117 10         122 $json = $self->{_json}->encode($obj);
118 8         30 $self->_debug("JSON created: $json");
119 10 50       16 } or do {
120 10 100       30 if ($@) {
121 2         4 $self->{has_error} = 1;
122 2         2 $self->{error_string} = $@;
123             # Strip Perl's "at FILE line N" location, optional ", line/chunk N"
124             # suffix Perl appends when <> was active, and any multi-line carp
125             # stack trace that follows. /s lets .* span newlines.
126 2         16 $self->{error_string} =~ s/\s+at\s+\S+\s+line\s+\d+(?:,\s+\S+\s+(?:line|chunk)\s+\d+)?\..*\z//s;
127 2         6 $self->_debug("Error serializing json from \$obj:" . $self->{error_string});
128             }
129             };
130 10         29 return $json;
131             }
132              
133             sub _decode
134             {
135 27     27   521 my ($self, $json) = @_;
136              
137 27         62 $self->_debug("Deserializing JSON");
138 27         38 my $obj = undef;
139             eval {
140             $json = $self->{predecodehook}->($json)
141 27 50       61 if defined($self->{predecodehook});
142 27   66     103 $self->{_json} ||= _build_json();
143 27         254 $obj = $self->{_json}->decode($json);
144 20         62 $self->_debug("Deserializing successful:",Dumper($obj));
145 27 50       55 } or do {
146 27 100       64 if ($@) {
147 7         12 $self->{has_error} = 1;
148 7         14 $self->{error_string} = $@;
149             # Strip Perl's "at FILE line N" location, optional ", line/chunk N"
150             # suffix Perl appends when <> was active, and any multi-line carp
151             # stack trace that follows. /s lets .* span newlines.
152 7         133 $self->{error_string} =~ s/\s+at\s+\S+\s+line\s+\d+(?:,\s+\S+\s+(?:line|chunk)\s+\d+)?\..*\z//s;
153 7         26 $self->_debug("Error deserializing: ".$self->{error_string});
154             }
155             };
156 27         153 return $obj;
157             }
158              
159             sub new
160             {
161 38     38 1 443503 my ($class, $base_url, %parameters) = @_;
162 38 100       119 return undef unless $base_url; ## no critic (ProhibitExplicitReturnUndef)
163              
164 37         142 my %ua_opts = %parameters;
165 37         78 map { delete $parameters{$_}; } qw(user pass realm debug predecodehook);
  185         315  
166              
167 37         240 my $ua = LWP::UserAgent->new(%parameters);
168              
169             my $self = bless ({
170             base_url => $base_url,
171             user_agent => $ua,
172             has_error => 0,
173             error_string => '',
174             debug => $ua_opts{debug},
175             predecodehook => $ua_opts{predecodehook},
176 37   33     17494 }, ref ($class) || $class);
177              
178 37         142 my $server = $self->_server($base_url);
179 37 100       110 my $default_port = $base_url =~ m|^https://| ? 443 : 80;
180 37 100       146 $server .= ":$default_port" unless $server =~ /:\d+$/;
181             $ua->credentials($server, $ua_opts{realm}, $ua_opts{user}, $ua_opts{pass})
182 37 100 100     180 if ($ua_opts{realm} && $ua_opts{user} && $ua_opts{pass});
      100        
183              
184 37         294 return $self;
185             }
186              
187             sub get
188             {
189 12     12 1 10885 my ($self, $path, $data, $apphdr) = @_;
190 12 100       32 if ($data) {
191 2         11 my @qp = map { "$_=".uri_encode($data->{$_}, { encode_reserved => 1 }) } sort keys %$data;
  4         2740  
192 2         2198 $path .= "?".join("&", @qp);
193             }
194 12         38 $self->_http_req("GET", $path, undef, $apphdr);
195             }
196              
197             sub patch
198             {
199 4     4 1 5303 my ($self, $path, $data, $apphdr) = @_;
200 4         14 $self->_http_req("PATCH", $path, $data, $apphdr);
201             }
202              
203             sub put
204             {
205 4     4 1 4296 my ($self, $path, $data, $apphdr) = @_;
206 4         9 $self->_http_req("PUT", $path, $data, $apphdr);
207             }
208              
209             sub post
210             {
211 4     4 1 5791 my ($self, $path, $data, $apphdr) = @_;
212 4         13 $self->_http_req("POST", $path, $data, $apphdr);
213             }
214              
215             sub del
216             {
217 2     2 1 2216 my ($self, $path, $apphdr) = @_;
218 2         7 $self->_http_req("DELETE", $path, undef, $apphdr);
219             }
220              
221             sub url
222             {
223 30     30 1 69 my ($self, $path) = @_;
224 30         166 my $url = $self->{base_url} . "/$path";
225             # REGEX-FU: look through the URL, replace any matches of /+ with '/',
226             # as long as the previous character was not a ':'
227             # (e.g. http://example.com//api//mypath/ becomes http://example.com/api/mypath/
228 30         411 $url =~ s|(?
229 30         83 return $url;
230             }
231              
232             sub response
233             {
234 0     0 1 0 my ($self) = @_;
235              
236 0         0 return $self->{response};
237             }
238              
239             sub header
240             {
241 0     0 1 0 my ($self, $name) = @_;
242              
243 0 0       0 return unless( $self->{response} );
244              
245 0 0       0 unless( $name ) {
246 0         0 return $self->{response}->header_field_names;
247             }
248 0         0 return $self->{response}->header( $name );
249             }
250              
251             sub errstr
252             {
253 9     9 1 1481 my ($self) = @_;
254 9 100       22 return ! $self->was_success ? $self->{error_string} : '';
255             }
256              
257             sub was_success
258             {
259 11     11 1 24 my ($self) = @_;
260 11 100       103 return $self->{has_error} ? 0 : 1;
261             }
262              
263             1;
264              
265             __END__