File Coverage

blib/lib/Dist/Metadata/Dist.pm
Criterion Covered Total %
statement 130 140 92.8
branch 44 54 81.4
condition 15 20 75.0
subroutine 29 35 82.8
pod 23 23 100.0
total 241 272 88.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of Dist-Metadata
3             #
4             # This software is copyright (c) 2011 by Randy Stauner.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9 11     11   4505 use strict;
  11         15  
  11         253  
10 11     11   35 use warnings;
  11         13  
  11         538  
11              
12             package Dist::Metadata::Dist;
13             our $AUTHORITY = 'cpan:RWSTAUNER';
14             # ABSTRACT: Base class for format-specific implementations
15             $Dist::Metadata::Dist::VERSION = '0.927';
16 11     11   53 use Carp qw(croak carp); # core
  11         12  
  11         523  
17 11     11   4567 use CPAN::DistnameInfo 0.12 ();
  11         8442  
  11         281  
18 11     11   2376 use Path::Class 0.24 ();
  11         139125  
  11         244  
19 11     11   3165 use Try::Tiny 0.09;
  11         7161  
  11         7761  
20              
21              
22             sub new {
23 90     90 1 10044 my $class = shift;
24             my $self = {
25 90 50       300 @_ == 1 ? %{ $_[0] } : @_
  0         0  
26             };
27              
28 90         126 bless $self, $class;
29              
30 90         236 my $req = $class->required_attribute;
31             croak qq['$req' parameter required]
32 90 100 66     1361 if $req && !$self->{$req};
33              
34 85 100       200 if ( exists $self->{file_spec} ) {
35             # we just want the OS name ('Unix' or '')
36             $self->{file_spec} =~ s/^File::Spec(::)?//
37 12 100       40 if $self->{file_spec};
38             # blank is no good, use "Native" hack
39             $self->{file_spec} = 'Native'
40 12 100       65 if !$self->{file_spec};
41             }
42              
43 85         185 return $self;
44             }
45              
46              
47 9     9 1 431 sub default_file_spec { 'Native' }
48              
49              
50             sub determine_name_and_version {
51 71     71 1 79 my ($self) = @_;
52 71         186 $self->set_name_and_version( $self->parse_name_and_version( $self->root ) );
53 71         92 return;
54             }
55              
56              
57             sub determine_packages {
58 42     42 1 283 my ($self, @files) = @_;
59              
60             my $determined = try {
61 42     42   1328 my @dir_and_files = $self->physical_directory(@files);
62              
63             # return
64 42         373 $self->packages_from_directory(@dir_and_files);
65             }
66             catch {
67 0     0   0 carp("Error determining packages: $_[0]");
68 0         0 +{}; # return
69 42         355 };
70              
71 42         29088 return $determined;
72             }
73              
74              
75             sub extract_into {
76 37     37 1 81 my ($self, $dir, @files) = @_;
77              
78 37 100       106 @files = $self->list_files
79             unless @files;
80              
81 37         168 require File::Basename;
82              
83 37         40 my @disk_files;
84 37         151 foreach my $file (@files) {
85 57         137 my $ff = $self->path_class_file->new_foreign( $self->file_spec, $file );
86             # Translate dist format (relative path) to disk/OS format and prepend $dir.
87             # This dir_list + basename hack is probably ok because the paths in a dist
88             # should always be relative (if there *was* a volume we wouldn't want it).
89 57         9573 my $path = $self->path_class_file
90             ->new( $dir, $ff->dir->dir_list, $ff->basename );
91              
92 57         5206 $path->dir->mkpath(0, oct(700));
93              
94 57         11874 my $full_path = $path->stringify;
95 57 50       4665 open(my $fh, '>', $full_path)
96             or croak "Failed to open '$full_path' for writing: $!";
97 57         254 print $fh $self->file_content($file);
98              
99             # do we really want full path or do we want relative?
100 57         4715 push(@disk_files, $full_path);
101             }
102              
103 37 100       226 return (wantarray ? ($dir, @disk_files) : $dir);
104             }
105              
106              
107             sub file_content {
108 0     0 1 0 croak q[Method 'file_content' not defined];
109             }
110              
111              
112             sub file_checksum {
113 22     22 1 25 my ($self, $file, $type) = @_;
114 22   50     29 $type ||= 'md5';
115              
116 22         556 require Digest; # core
117              
118             # md5 => MD5, sha256 => SHA-256
119 22         476 (my $impl = uc $type) =~ s/^(SHA|CRC)([0-9]+)$/$1-$2/;
120              
121 22         70 my $digest = Digest->new($impl);
122              
123 22         3576 $digest->add( $self->file_content($file) );
124 22         1839 return $digest->hexdigest;
125             }
126              
127              
128             sub find_files {
129 0     0 1 0 croak q[Method 'find_files' not defined];
130             }
131              
132              
133             sub file_spec {
134 219     219 1 1722 my ($self) = @_;
135              
136             $self->{file_spec} = $self->default_file_spec
137 219 100       485 if !exists $self->{file_spec};
138              
139 219         970 return $self->{file_spec};
140             }
141              
142              
143             sub full_path {
144 102     102 1 107 my ($self, $file) = @_;
145              
146 102 100       189 return $file
147             unless my $root = $self->root;
148              
149             # don't re-add the root if it's already there
150 64 50       488 return $file
151             # FIXME: this regexp is probably not cross-platform...
152             # FIXME: is there a way to do this with File::Spec?
153             if $file =~ m@^\Q${root}\E[\\/]@;
154              
155             # FIXME: does this foreign_file work w/ Dir ?
156 64         130 return $self->path_class_file
157             ->new_foreign($self->file_spec, $root, $file)->stringify;
158             }
159              
160              
161             sub list_files {
162 105     105 1 1558 my ($self) = @_;
163              
164             $self->{_list_files} = do {
165 59         182 my @files = sort $self->find_files;
166 59         567 my ($root, @rel) = $self->remove_root_dir(@files);
167 59         114 $self->{root} = $root;
168 59         144 \@rel; # return
169             }
170 105 100       242 unless $self->{_list_files};
171              
172 105         105 return @{ $self->{_list_files} };
  105         307  
173             }
174              
175              
176             {
177 11     11   56 no strict 'refs'; ## no critic (NoStrict)
  11         15  
  11         8210  
178             foreach my $method ( qw(
179             name
180             version
181             ) ){
182             *$method = sub {
183 128     128   459 my ($self) = @_;
184              
185             $self->determine_name_and_version
186 128 100       389 if !exists $self->{ $method };
187              
188 128         268 return $self->{ $method };
189             };
190             }
191             }
192              
193              
194             sub packages_from_directory {
195 42     42 1 102 my ($self, $dir, @files) = @_;
196              
197 42         75 my @pvfd = ($dir);
198             # M::M::p_v_f_d expects full paths for \@files
199             push @pvfd, [map {
200 42 50       154 $self->path_class_file->new($_)->is_absolute
  65 50       1991  
201             ? $_ : $self->path_class_file->new($dir, $_)->stringify
202             } @files]
203             if @files;
204              
205 42         6931 require Module::Metadata;
206              
207             my $provides = try {
208 42     42   1166 my $packages = Module::Metadata->package_versions_from_directory(@pvfd);
209 42         41878 while ( my ($pack, $pv) = each %$packages ) {
210             # M::M::p_v_f_d returns files in native OS format (obviously);
211             # CPAN::Meta expects file paths in Unix format
212             $pv->{file} = $self->path_class_file
213 77         7048 ->new($pv->{file})->as_foreign('Unix')->stringify;
214             }
215 42         8065 $packages; # return
216             }
217             catch {
218 0     0   0 carp("Failed to determine packages: $_[0]");
219 0         0 +{}; # return
220 42         29465 };
221 42   50     934 return $provides || {};
222             }
223              
224              
225             sub parse_name_and_version {
226 115     115 1 4244 my ($self, $path) = @_;
227 115         100 my ( $name, $version );
228 115 100       219 if ( $path ){
229             # try a simple regexp first
230 76 100       527 $path =~ m!
231             ([^\\/]+) # name (anything below final directory)
232             - # separator
233             (v?[0-9._]+) # version
234             (?: # possible file extensions
235             \.t(?:ar\.)?gz
236             )?
237             $
238             !x and
239             ( $name, $version ) = ( $1, $2 );
240              
241             # attempt to improve data with CPAN::DistnameInfo (but ignore any errors)
242             # TODO: also grab maturity and cpanid ?
243             # release_status = $dist->maturity eq 'released' ? 'stable' : 'unstable';
244             # -(TRIAL|RC) => 'testing', '_' => 'unstable'
245 76         84 eval {
246             # DistnameInfo expects any directories in unix format (thanks jeroenl)
247 76         217 my $dnifile = $self->path_class_file
248             ->new($path)->as_foreign('Unix')->stringify;
249             # if it doesn't appear to have an extension fake one to help DistnameInfo
250 76 100       12324 $dnifile .= '.tar.gz' unless $dnifile =~ /\.[a-z]\w+$/;
251              
252 76         319 my $dni = CPAN::DistnameInfo->new($dnifile);
253 76         3805 my $dni_name = $dni->dist;
254 76         267 my $dni_version = $dni->version;
255             # if dni matched both name and version, or previous regexp didn't match
256 76 50 66     752 if ( $dni_name && $dni_version || !$name ) {
      66        
257 76 50       144 $name = $dni_name if $dni_name;
258 76 100       311 $version = $dni_version if $dni_version;
259             }
260             };
261 76 50       154 warn $@ if $@;
262             }
263 115         348 return ($name, $version);
264             }
265              
266              
267 23   100 23 1 156 sub path_class_dir { $_[0]->{path_class_dir} ||= 'Path::Class::Dir' }
268 533   100 533 1 2324 sub path_class_file { $_[0]->{path_class_file} ||= 'Path::Class::File' }
269              
270              
271             sub path_classify_dir {
272 0     0 1 0 my ($self, $dir) = @_;
273 0         0 $self->path_class_dir->new_foreign($self->file_spec, $dir)
274             }
275              
276             sub path_classify_file {
277 88     88 1 113 my ($self, $file) = @_;
278 88         198 $self->path_class_file->new_foreign($self->file_spec, $file)
279             }
280              
281              
282             sub perl_files {
283             return
284 43     43 1 136 grep { /\.pm$/ }
  148         368  
285             $_[0]->list_files;
286             }
287              
288              
289             sub physical_directory {
290 37     37 1 1107 my ($self, @files) = @_;
291              
292 37         196 require File::Temp;
293             # dir will be removed when return value goes out of scope (in caller)
294 37         268 my $dir = File::Temp->newdir();
295              
296 37         14429 return $self->extract_into($dir, @files);
297             }
298              
299              
300             sub remove_root_dir {
301 59     59 1 118 my ($self, @files) = @_;
302 59 50       139 return unless @files;
303              
304             # FIXME: can we use File::Spec for these regexp's instead of [\\/] ?
305              
306             # grab the root dir from the first file
307 59 100       359 $files[0] =~ m{^([^\\/]+)[\\/]}
308             # if not matched quit now
309             or return (undef, @files);
310              
311 32         73 my $dir = $1;
312 32         35 my @rel;
313              
314             # strip $dir from each file
315 32         75 for (@files) {
316              
317 98 50       687 m{^\Q$dir\E[\\/](.+)$}
318             # if the match failed they're not all under the same root so just return now
319             or return (undef, @files);
320              
321 98         193 push @rel, $1;
322             }
323              
324 32         106 return ($dir, @rel);
325              
326             }
327              
328              
329 0     0 1 0 sub required_attribute { return }
330              
331              
332             sub root {
333 193     193 1 691 my ($self) = @_;
334              
335             # call list_files instead of find_files so that it caches the result
336             $self->list_files
337 193 100       364 unless exists $self->{root};
338              
339 193         565 return $self->{root};
340             }
341              
342              
343             sub set_name_and_version {
344 115     115 1 5272 my ($self, @values) = @_;
345 115         173 my @fields = qw( name version );
346              
347 115         272 foreach my $i ( 0 .. $#fields ){
348             $self->{ $fields[$i] } = $values[$i]
349 230 100 100     895 if !exists $self->{ $fields[$i] } && defined $values[$i];
350             }
351 115         217 return;
352             }
353              
354              
355             # version() defined with name()
356              
357             1;
358              
359             __END__