File Coverage

blib/lib/WWW/PGXN.pm
Criterion Covered Total %
statement 167 170 98.2
branch 48 56 85.7
condition 14 19 73.6
subroutine 45 45 100.0
pod 14 14 100.0
total 288 304 94.7


line stmt bran cond sub pod time code
1             package WWW::PGXN;
2              
3 9     9   912920 use 5.010;
  9         36  
4 9     9   58 use strict;
  9         80  
  9         314  
5 9     9   5612 use WWW::PGXN::Distribution;
  9         30  
  9         349  
6 9     9   4651 use WWW::PGXN::Extension;
  9         28  
  9         511  
7 9     9   4272 use WWW::PGXN::User;
  9         33  
  9         305  
8 9     9   3962 use WWW::PGXN::Tag;
  9         29  
  9         302  
9 9     9   4224 use WWW::PGXN::Mirror;
  9         26  
  9         301  
10 9     9   6866 use HTTP::Tiny;
  9         525134  
  9         480  
11 9     9   4604 use URI::Template;
  9         132934  
  9         606  
12 9     9   6441 use JSON ();
  9         106277  
  9         379  
13 9     9   96 use Carp;
  9         17  
  9         9655  
14              
15             our $VERSION = v0.13.0;
16              
17             sub new {
18 9     9 1 2012548 my($class, %params) = @_;
19 9         34 my $self = bless {} => $class;
20 9         34 for my $key (qw(url proxy)) {
21 18 100       127 $self->$key($params{$key}) if exists $params{$key}
22             }
23 9         43 return $self;
24             }
25              
26             sub get_distribution {
27 15     15 1 516 my ($self, $dist, $version) = @_;
28 15 100 100     149 my $data = $self->_fetch_json(
    100          
29             (defined $version ? 'meta' : 'dist'),
30             { dist => lc $dist, version => lc($version || '') }
31             ) or return;
32 14         237 WWW::PGXN::Distribution->new($self, $data);
33             }
34              
35             sub get_extension {
36 2     2 1 753 my ($self, $ext) = @_;
37 2 100       15 my $data = $self->_fetch_json(extension => { extension => lc $ext })
38             or return;
39 1         12 WWW::PGXN::Extension->new($self, $data);
40             }
41              
42             sub get_user {
43 2     2 1 1538 my ($self, $user) = @_;
44 2 100       15 my $data = $self->_fetch_json(user => { user => lc $user }) or return;
45 1         11 WWW::PGXN::User->new($data);
46             }
47              
48             sub get_tag {
49 2     2 1 768 my ($self, $tag) = @_;
50 2 100       14 my $data = $self->_fetch_json(tag => { tag => lc $tag }) or return;
51 1         14 WWW::PGXN::Tag->new($data);
52             }
53              
54             sub get_stats {
55 2     2 1 753 my ($self, $name) = @_;
56 2 100       14 my $data = $self->_fetch_json(stats => { stats => lc $name }) or return;
57             }
58              
59             sub get_userlist {
60 3     3 1 1465 my ($self, $letter) = @_;
61 3 100       11 return undef unless $self->_uri_templates->{userlist};
62 2   100     38 return $self->_fetch_json(userlist => { letter => lc $letter }) || [];
63             }
64              
65             my %valid_in = ( map { $_ => undef } qw(docs dists extensions users tags));
66              
67             sub search {
68 12     12 1 20733 my ($self, %params) = @_;
69 12         62 my $url = $self->url;
70             my $in = delete $params{in}
71 12 100       251 or croak 'Missing required "in" parameter to search()';
72              
73             croak qq{Invalid "in" parameter to search(); Must be one of:\n}
74 5         158 . join("\n", map { "* $_" } sort keys %valid_in)
75 11 100       41 unless exists $valid_in{$in};
76              
77 10 100       42 if ($url->scheme eq 'file') {
78             # Fetch it via PGXN::API::Searcher.
79 5   66     135 my $searcher = $self->{_searcher} ||= PGXN::API::Searcher->new(
80             File::Spec->catdir($url->path_segments)
81             );
82 5         108 return $searcher->search(in => $in, %params);
83             }
84              
85 5         127 my $qurl = $self->_url_for(search => { in => $in });
86             $qurl->query_form({
87 5         1460 map { substr($_, 0, 1) => $params{$_} } keys %params
  15         64  
88             });
89 5 50       743 my $res = $self->_fetch($qurl) or return;
90 5         121 return JSON->new->utf8->decode($res->{content});
91             }
92              
93             sub mirrors {
94 1     1 1 768 my $self = shift;
95 1   33     2 return @{ $self->{mirrors} ||= do {
  1         7  
96 1         21 my $mirrors = $self->_fetch_json('mirrors');
97 1         3 [ map { WWW::PGXN::Mirror->new($_) } @{ $mirrors } ];
  2         14  
  1         3  
98             } };
99             }
100              
101             sub spec {
102 3     3 1 1359 my ($self, $format) = @_;
103 3   100     16 $format ||= 'txt';
104 3 50       19 my $res = $self->_fetch(
105             $self->_url_for('spec' => { format => $format })
106             ) or return;
107 3         24 utf8::decode $res->{content};
108 3         32 return $res->{content};
109             }
110              
111             sub url {
112 106     106 1 4373 my $self = shift;
113 106 100       545 return $self->{url} unless @_;
114 13         92 (my $url = shift) =~ s{/+$}{}g;
115 13         136 $self->{url} = URI->new($url);
116 13 100       11497 require PGXN::API::Searcher if $self->{url}->scheme eq 'file';
117 13         1568 delete $self->{_req};
118 13         33 delete $self->{_searcher};
119 13         49 $self->{url};
120             }
121              
122             sub proxy {
123 2     2 1 33 my $self = shift;
124 2 50       22 return $self->{proxy} unless @_;
125 0         0 $self->{proxy} = shift;
126             }
127              
128             BEGIN {
129 9     9   34 for my $thing (qw(meta download source)) {
130 9     9   71 no strict 'refs';
  9         19  
  9         1386  
131 27         112 *{"$thing\_url_for"} = sub {
132 3     3   3593 $_[0]->_url_for( $thing => { dist => lc $_[1], version => lc $_[2] });
133 27         87 };
134 27         97 *{"$thing\_path_for"} = sub {
135 3     3   4151 $_[0]->_path_for( $thing => { dist => lc $_[1], version => lc $_[2] });
136 27         72 };
137             }
138              
139 9         24 for my $thing (qw(tag extension user)) {
140 9     9   57 no strict 'refs';
  9         20  
  9         1012  
141 27         86 *{"$thing\_url_for"} = sub {
142 3     3   3836 $_[0]->_url_for( $thing => { $thing => lc $_[1] });
143 27         60 };
144 27         7606 *{"$thing\_path_for"} = sub {
145 3     3   3588 $_[0]->_path_for( $thing => { $thing => lc $_[1] });
146 27         66 };
147             }
148             }
149              
150             sub html_doc_path_for {
151 2     2 1 1145 my ($self, $dist, $version, $path) = @_;
152 2         17 $self->_path_for(htmldoc => {
153             dist => lc $dist,
154             version => lc $version,
155             docpath => $path,
156             });
157             }
158              
159             sub html_doc_url_for {
160 1     1 1 1203 my $self = shift;
161 1         5 return URI->new($self->url . $self->html_doc_path_for(@_));
162             }
163              
164             sub _uri_templates {
165 79     79   1205 my $self = shift;
166 79   100     852 return $self->{uri_templates} ||= { do {
167 9         36 my $req = $self->_request;
168 9         54 my $url = URI->new($self->url . '/index.json');
169 9         732 my $res = $req->get($url);
170             croak "Request for $url failed: $res->{status}: $res->{reason}\n"
171 9 100       88 unless $res->{success};
172 8         1295 my $tmpl = JSON->new->utf8->decode($res->{content});
173 8         102 map { $_ => URI::Template->new($tmpl->{$_}) } keys %{ $tmpl };
  96         11987  
  8         51  
174             }};
175             }
176              
177             sub _path_for {
178 63     63   164 my ($self, $name, $vars) = @_;
179 63 100       190 my $tmpl = $self->_uri_templates->{$name}
180             or croak qq{No URI template named "$name"};
181 62         2118 return $tmpl->process($vars);
182             }
183              
184             sub _url_for {
185 54     54   7534 my $self = shift;
186 54         175 return URI->new($self->url . $self->_path_for(@_));
187             }
188              
189             sub _request {
190 53     53   102 my $self = shift;
191 53 100 66     277 $self->{_req} ||= $self->url =~ m{^file:} ? WWW::PGXN::FileReq->new : HTTP::Tiny->new(
192             agent => __PACKAGE__ . '/' . __PACKAGE__->VERSION,
193             proxy => $self->proxy,
194             );
195             }
196              
197             sub _fetch {
198 42     42   11346 my ($self, $url) = @_;
199 42         115 my $res = $self->_request->get($url);
200 42 100       256 return $res if $res->{success};
201 6 50       225 return if $res->{status} == 404;
202 0         0 croak "Request for $url failed: $res->{status}: $res->{reason}\n";
203             }
204              
205             sub _fetch_json {
206 32     32   70 my $self = shift;
207 32 100       110 my $res = $self->_fetch($self->_url_for(@_)) or return;
208 26         893 return JSON->new->utf8->decode($res->{content});
209             }
210              
211             sub _download_to {
212 5     5   15 my ($self, $file) = (shift, shift);
213 5         19 my $url = $self->_url_for(download => @_);
214 5         2203 my $res = $self->_fetch($url);
215 5 100       64 if (-e $file) {
216 2 50       45 if (-d $file) {
217 2         10 my @seg = $url->path_segments;
218 2         197 $file = File::Spec->catfile($file, $seg[-1]);
219             } else {
220 0         0 croak "$file already exists";
221             }
222             }
223              
224 5 50       1519 open my $fh, '>:raw', $file or die "Cannot open $file: $!\n";
225 5         52 print $fh $res->{content};
226 5 50       332 close $fh or die "Cannot close $file: $!\n";
227 5         115 return $file;
228             }
229              
230             package
231             WWW::PGXN::FileReq;
232              
233 9     9   72 use strict;
  9         21  
  9         259  
234 9     9   4819 use URI::file ();
  9         75677  
  9         264  
235 9     9   68 use File::Spec ();
  9         18  
  9         129  
236 9     9   36 use URI::Escape ();
  9         18  
  9         1817  
237              
238             sub new {
239 10     10   289 bless {} => shift;
240             }
241              
242             sub get {
243 53     53   290 my $self = shift;
244 53         217 my $file = File::Spec->catfile(shift->path_segments);
245              
246             return {
247 53 100       19419 success => 0,
248             status => 404,
249             reason => 'not found',
250             headers => {},
251             } unless -e $file;
252              
253 45 50       2359 open my $fh, '<:raw', $file or return {
254             success => 0,
255             status => 500,
256             reason => $!,
257             headers => {},
258             };
259              
260 45         246 local $/;
261             return {
262 45   50     15150 success => 1,
263             status => 200,
264             reason => 'OK',
265             content => <$fh> || undef,
266             headers => {},
267             };
268             }
269              
270             1;
271             __END__