File Coverage

blib/lib/Mojito/Model/MetaCPAN.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1 2     2   25874 use strictures 1;
  2         14  
  2         52  
2              
3             package Mojito::Model::MetaCPAN;
4             {
5             $Mojito::Model::MetaCPAN::VERSION = '0.24';
6             }
7 2     2   1228 use Moo;
  2         17673  
  2         15  
8 2     2   10327 use HTTP::Tiny;
  2         117725  
  2         90  
9 2     2   1752 use MetaCPAN::API;
  2         276474  
  2         135  
10 2     2   3927 use Text::MultiMarkdown;
  2         107602  
  2         162  
11 2     2   2960 use CHI;
  0            
  0            
12             use Data::Dumper::Concise;
13              
14             =head1 Name
15              
16             Mojito::Model::MetaCPAN - Tap into metacpan.org
17              
18             =cut
19              
20             has http_client => (
21             is => 'ro',
22             lazy => 1,
23             default => sub { HTTP::Tiny->new },
24             );
25              
26             has metacpan => (
27             is => 'ro',
28             lazy => 1,
29             default => sub { MetaCPAN::API->new },
30             );
31              
32             has cache => (
33             is => 'rw',
34             default => sub {
35             CHI->new(
36             driver => 'Memory',
37             global => 1,
38             # driver => 'File',
39             root_dir => '/tmp/mojito/cache',
40             );
41             },
42             );
43              
44             has not_found_string => (
45             is => 'rw',
46             lazy => 1,
47             'default' => sub { 'NOT FOUND' },
48             );
49              
50             has markdown => (
51             is => 'ro',
52             lazy => 1,
53             'default' => sub { return Text::MultiMarkdown->new }
54             );
55              
56             =head1 Methods
57              
58             =head2 get_synopsis_from_metacpan
59              
60             Args: (Str: main module path, ModuleName: main module)
61             Returns: (Array: ($pod_url_used, @synopsis_lines)
62              
63             =cut
64              
65             sub get_synopsis_from_metacpan {
66             my ($self, $main_module, $pod_url) = @_;
67              
68             my $pod_url_used = 'release';
69             # Use markdown format (easy to parse out SYNOPSIS)
70             # my $format = '?content-type=text/x-markdown';
71             my $format = '?content-type=text/plain';
72             my $secondary_pod_url = "http://api.metacpan.org/pod/${main_module}${format}";
73             if (not $pod_url) {
74             $pod_url = $secondary_pod_url;
75             $pod_url_used = 'main_module';
76             }
77             else {
78             $pod_url .= $format;
79             }
80             my $response = $self->http_client->get($pod_url);
81             if (not $response->{success} && ($pod_url_used = 'release')) {
82             warn "Could not find POD at $pod_url. Trying: $secondary_pod_url..";
83             $response = $self->http_client->get($secondary_pod_url);
84             if (not $response->{success}) {
85             warn "Could not find POD at $secondary_pod_url";
86             return $self->not_found_string;
87             }
88             }
89             my $content = $response->{content}; # if length $response->{content};
90             my @synopsis_lines = ();
91             my $seen_synopsis = my $seen_synopsis_end = 0;
92             my @content_lines = split '\n', $content;
93              
94             foreach (@content_lines) {
95             # Are we starting the section after the Synopsis?
96             # if ($seen_synopsis && m/^#\s/) {
97             if ($seen_synopsis && m/^\S/) {
98             $seen_synopsis_end = 1;
99             }
100             # if (m/^#\s+SYNOPSIS/i) {
101             if (m/^SYNOPSIS/i) {
102             $seen_synopsis = 1;
103             }
104             if ($seen_synopsis && not $seen_synopsis_end) {
105             use Encode qw/ decode_utf8 encode_utf8/;
106             push @synopsis_lines, decode_utf8($_);
107             # push @synopsis_lines, $_;
108             }
109             }
110             return wantarray ? @synopsis_lines : join "\n", @synopsis_lines;
111             }
112              
113             sub get_description_from_metacpan {
114             my ($self, $Module) = @_;
115            
116             my $pod_url =
117             "http://api.metacpan.org/pod/${Module}?content-type=text/x-markdown";
118             my $response = $self->http_client->get($pod_url);
119             if (not $response->{success}) {
120             #warn "Failed to get URL: $pod_url";
121             return;
122             }
123             my $content = $response->{content}; # if length $response->{content};
124             my @description_lines = ();
125             my $seen_description = my $seen_description_end = 0;
126             my @content_lines = split '\n', $content;
127              
128             foreach (@content_lines) {
129            
130             # Are we starting the section after the Synopsis?
131             if ($seen_description && m/^#\s/) {
132             $seen_description_end = 1;
133             }
134             if (m/^#\s+DESCRIPTION/i) {
135             $seen_description = 1;
136             }
137             if ($seen_description && not $seen_description_end) {
138             push @description_lines, $_;
139             }
140             }
141             return wantarray ? @description_lines : join "\n", @description_lines;
142             }
143            
144             =head2 get_synopsis_formatted
145              
146             signature: (a Perl Module name, an element of qw/presentation/)
147             example: my $synop = $self->get_synopsis_formatted('Moose', 'presentation');
148            
149             =cut
150              
151             sub get_synopsis_formatted {
152             my ($self, $release, $format) = @_;
153             $format ||= 'presentation';
154            
155             my $main_module_pod_url = my $main_module = $release->{distribution};
156             $main_module =~ s|-|::|g;
157             $main_module_pod_url =~ s|-|/|g;
158             $main_module_pod_url = "http://api.metacpan.org/pod/$release->{author}/$release->{name}/lib/${main_module_pod_url}.pm";
159             # Just have the presentation format for starters.
160             my $dispatch_table = {
161             presentation => sub {
162             my @synopsis_lines = $self->get_synopsis_from_metacpan($main_module, $main_module_pod_url);
163             @synopsis_lines = $self->trim_lines(@synopsis_lines);
164             if (not scalar @synopsis_lines) {
165             return $self->not_found_string;
166             }
167             my $abstract = '';
168             $abstract = "<section class='module_abstract'>$release->{abstract}</section>\n" if $release->{abstract};
169             # Comment out lines that don't start with a comment
170             # and are not indented (i.e. not code)
171             # because we'd like the Synopsis to be runnable (in theory)
172             my ($whitespace) = $synopsis_lines[0] =~ m/^(\s*)/;
173             @synopsis_lines = map { my $line = $_; $line =~ s/^(\w)/&#35; $1/; $line; } @synopsis_lines;
174              
175             # Trim off leading whitespace (usually 2 or 4)
176             @synopsis_lines = map { my $line = $_; $line =~ s/^$whitespace//; $line; } @synopsis_lines;
177             my $synopsis = join "\n", @synopsis_lines;
178              
179             # pre wrapper for syntax highlight
180             $synopsis = "${abstract}<pre class='sh_perl'>\n" . $synopsis . "</pre>\n";
181             $synopsis = "<h2 class='Module'><a href='https://metacpan.org/release/$release->{author}/$release->{name}'>$release->{distribution}</a></h2>" . $synopsis;
182            
183             return $synopsis;
184             }
185             };
186              
187             my $cache_key = "$release->{name}:SYNOPSIS:${format}";
188             my $synopsis = $self->cache->get($cache_key);
189             if (not $synopsis) {
190             warn "GET $main_module SYNOPSIS from CPAN" if $ENV{MOJITO_DEBUG};
191             $synopsis = $dispatch_table->{$format}->($main_module_pod_url);
192             $self->cache->set($cache_key, $synopsis, '3 days');
193             }
194             return $synopsis;
195             }
196              
197             =head2 trim_lines
198              
199             Remove first line
200             Remove leading and trailing blank lines
201              
202             =cut
203              
204             sub trim_lines {
205             my ($self, @lines) = @_;
206              
207             return if not scalar @lines;
208              
209             # Get rid of first line and any blank line directly after
210             # We'll rewrite the first line and are making the results more
211             # compact by removing the blank lines.
212             shift @lines;
213             return if not scalar @lines;
214             while ($lines[0] && $lines[0] =~ m/^\s*?$/) {
215             shift @lines;
216             }
217             return if not scalar @lines;
218              
219             # Do same for tail
220             while ($lines[-1] && $lines[-1] =~ m/^\s*?$/) {
221             pop @lines;
222             }
223             return if not scalar @lines;
224             return @lines;
225             }
226             =head2 get_recent_releases_from_metacpan
227              
228             Get an ArrayRef[HashRef] of the most recent CPAN releases
229              
230             =cut
231              
232             sub get_recent_releases_from_metacpan {
233             my ($self, $how_many) = @_;
234             $how_many ||= 10;
235              
236             my @fields = qw/author name distribution version maturity status abstract download_url/;
237             my $fields_string = join ',', @fields;
238             my $result = $self->metacpan->release(
239             search => {
240             sort => "date:desc",
241             fields => $fields_string,
242             size => $how_many,
243             },
244             );
245              
246             return [ map { $_->{fields} } @{ $result->{hits}->{hits} } ];
247             }
248              
249             =head2 recent_synopses_shortcut
250              
251             Create the Mojito shortcut that gets the synopses of the most
252             recently released CPAN distributions. Looks like:
253            
254             {{synopsis Module1}}
255             {{synopsis Module2}}
256             ...
257             {{synopsis Modulen}}
258            
259             =cut
260              
261             sub recent_synopses_shortcut {
262             my ($self, $how_many) = @_;
263             $how_many ||= 10;
264              
265             my $cache_key = "CPAN_RECENT_SYNOPSES:${how_many}";
266             my $synopses = $self->cache->get($cache_key);
267             if (not $synopses) {
268             warn "GET Recent Release from CPAN" if $ENV{MOJITO_DEBUG};
269             my @releases = $self->get_recent_releases_from_metacpan($how_many);
270             my @recent_synopses = map { "{{cpan.synopsis $_}}" }
271             map {
272             my $dist = $_->{distribution};
273             $dist =~ s/\-/::/g;
274             $dist;
275             } @releases;
276             $synopses = join "\n", @recent_synopses;
277             $self->cache->set($cache_key, $synopses, '1 minute');
278             }
279             return $synopses;
280             }
281              
282             =head2 get_recent_releases
283              
284             Get the most recent releases (as module names)
285            
286             =cut
287              
288             sub get_recent_releases {
289             my ($self, $how_many) = @_;
290             $how_many ||= 10;
291              
292             my $cache_key = "CPAN_RECENT_RELEASES:${how_many}";
293             my $releases = $self->cache->get($cache_key);
294             if (not $releases) {
295             warn "GET Recent Releases from CPAN\n" if $ENV{MOJITO_DEBUG};
296             $releases = $self->get_recent_releases_from_metacpan($how_many);
297             $self->cache->set($cache_key, $releases, '1 minute');
298             }
299             return $releases;
300             }
301              
302             =head2 get_recent_synopses
303              
304             Get the most recent synopses from CPAN
305            
306             =cut
307              
308             sub get_recent_synopses {
309             my ($self, $how_many) = @_;
310             $how_many ||= 10;
311             my $metacpan_web_host = 'https://metacpan.org';
312              
313             my $html;
314             my $releases = $self->get_recent_releases($how_many);
315             # Avoid duplicates
316             my %have_seen = ();
317             foreach my $release (@{$releases}) {
318             my $main_module = $release->{distribution};
319             $main_module =~ s/\-/::/g;
320             next if $have_seen{$main_module};
321             my $synopsis = $self->get_synopsis_formatted($release, 'presentation');
322             my $not_found_message = '';
323             if ($synopsis eq $self->not_found_string) {
324             $not_found_message = 'Synopsis not found for ';
325             if ($release->{maturity} eq 'released') {
326             $html .= "<section class='released'>$not_found_message
327             <a href='${metacpan_web_host}/release/$release->{author}/$release->{name}'>$release->{name}</a>
328             </section>\n";
329             }
330             else {
331             $html .= "<section class='developer'>$not_found_message
332             <a href='${metacpan_web_host}/release/$release->{author}/$release->{name}'>$release->{name}</a>
333             <span style='font-size: 88%;'> (dev)</span>
334             </section>\n";
335             }
336             }
337             else {
338             $html .= $synopsis;
339             }
340             $have_seen{$main_module}++;
341             }
342             return $html;
343             }
344              
345             1