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   4828 use strict;
  11         19  
  11         353  
10 11     11   65 use warnings;
  11         14  
  11         544  
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.926';
16 11     11   52 use Carp qw(croak carp); # core
  11         15  
  11         648  
17 11     11   5003 use CPAN::DistnameInfo 0.12 ();
  11         18346  
  11         310  
18 11     11   2634 use Path::Class 0.24 ();
  11         175702  
  11         267  
19 11     11   4560 use Try::Tiny 0.09;
  11         8747  
  11         8421  
20              
21              
22             sub new {
23 90     90 1 11389 my $class = shift;
24 0         0 my $self = {
25 90 50       429 @_ == 1 ? %{ $_[0] } : @_
26             };
27              
28 90         222 bless $self, $class;
29              
30 90         325 my $req = $class->required_attribute;
31 90 100 66     1829 croak qq['$req' parameter required]
32             if $req && !$self->{$req};
33              
34 85 100       300 if ( exists $self->{file_spec} ) {
35             # we just want the OS name ('Unix' or '')
36 12 100       44 $self->{file_spec} =~ s/^File::Spec(::)?//
37             if $self->{file_spec};
38             # blank is no good, use "Native" hack
39 12 100       30 $self->{file_spec} = 'Native'
40             if !$self->{file_spec};
41             }
42              
43 85         204 return $self;
44             }
45              
46              
47 9     9 1 10717 sub default_file_spec { 'Native' }
48              
49              
50             sub determine_name_and_version {
51 71     71 1 84 my ($self) = @_;
52 71         330 $self->set_name_and_version( $self->parse_name_and_version( $self->root ) );
53 71         99 return;
54             }
55              
56              
57             sub determine_packages {
58 42     42 1 307 my ($self, @files) = @_;
59              
60             my $determined = try {
61 42     42   1823 my @dir_and_files = $self->physical_directory(@files);
62              
63             # return
64 42         464 $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         465 };
70              
71 42         34465 return $determined;
72             }
73              
74              
75             sub extract_into {
76 37     37 1 102 my ($self, $dir, @files) = @_;
77              
78 37 100       122 @files = $self->list_files
79             unless @files;
80              
81 37         197 require File::Basename;
82              
83 37         45 my @disk_files;
84 37         125 foreach my $file (@files) {
85 57         226 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         12349 my $path = $self->path_class_file
90             ->new( $dir, $ff->dir->dir_list, $ff->basename );
91              
92 57         6810 $path->dir->mkpath(0, oct(700));
93              
94 57         151304 my $full_path = $path->stringify;
95 57 50       6018 open(my $fh, '>', $full_path)
96             or croak "Failed to open '$full_path' for writing: $!";
97 57         332 print $fh $self->file_content($file);
98              
99             # do we really want full path or do we want relative?
100 57         6508 push(@disk_files, $full_path);
101             }
102              
103 37 100       277 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 45 my ($self, $file, $type) = @_;
114 22   50     47 $type ||= 'md5';
115              
116 22         1028 require Digest; # core
117              
118             # md5 => MD5, sha256 => SHA-256
119 22         853 (my $impl = uc $type) =~ s/^(SHA|CRC)([0-9]+)$/$1-$2/;
120              
121 22         108 my $digest = Digest->new($impl);
122              
123 22         5890 $digest->add( $self->file_content($file) );
124 22         3286 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 1763 my ($self) = @_;
135              
136 219 100       741 $self->{file_spec} = $self->default_file_spec
137             if !exists $self->{file_spec};
138              
139 219         1804 return $self->{file_spec};
140             }
141              
142              
143             sub full_path {
144 102     102 1 180 my ($self, $file) = @_;
145              
146 102 100       325 return $file
147             unless my $root = $self->root;
148              
149             # don't re-add the root if it's already there
150 64 50       745 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         311 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 1878 my ($self) = @_;
163              
164 105 100       330 $self->{_list_files} = do {
165 59         237 my @files = sort $self->find_files;
166 59         754 my ($root, @rel) = $self->remove_root_dir(@files);
167 59         159 $self->{root} = $root;
168 59         167 \@rel; # return
169             }
170             unless $self->{_list_files};
171              
172 105         205 return @{ $self->{_list_files} };
  105         450  
173             }
174              
175              
176             {
177 11     11   83 no strict 'refs'; ## no critic (NoStrict)
  11         22  
  11         9080  
178             foreach my $method ( qw(
179             name
180             version
181             ) ){
182             *$method = sub {
183 128     128   895 my ($self) = @_;
184              
185 128 100       473 $self->determine_name_and_version
186             if !exists $self->{ $method };
187              
188 128         329 return $self->{ $method };
189             };
190             }
191             }
192              
193              
194             sub packages_from_directory {
195 42     42 1 127 my ($self, $dir, @files) = @_;
196              
197 42         71 my @pvfd = ($dir);
198             # M::M::p_v_f_d expects full paths for \@files
199 65 50       2506 push @pvfd, [map {
200 42 50       158 $self->path_class_file->new($_)->is_absolute
201             ? $_ : $self->path_class_file->new($dir, $_)->stringify
202             } @files]
203             if @files;
204              
205 42         8650 require Module::Metadata;
206              
207             my $provides = try {
208 42     42   1530 my $packages = Module::Metadata->package_versions_from_directory(@pvfd);
209 42         64707 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 77         10473 $pv->{file} = $self->path_class_file
213             ->new($pv->{file})->as_foreign('Unix')->stringify;
214             }
215 42         10164 $packages; # return
216             }
217             catch {
218 0     0   0 carp("Failed to determine packages: $_[0]");
219 0         0 +{}; # return
220 42         34679 };
221 42   50     1132 return $provides || {};
222             }
223              
224              
225             sub parse_name_and_version {
226 115     115 1 4717 my ($self, $path) = @_;
227 115         108 my ( $name, $version );
228 115 100       229 if ( $path ){
229             # try a simple regexp first
230 76 100       733 $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         126 eval {
246             # DistnameInfo expects any directories in unix format (thanks jeroenl)
247 76         296 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       19325 $dnifile .= '.tar.gz' unless $dnifile =~ /\.[a-z]\w+$/;
251              
252 76         441 my $dni = CPAN::DistnameInfo->new($dnifile);
253 76         5235 my $dni_name = $dni->dist;
254 76         346 my $dni_version = $dni->version;
255             # if dni matched both name and version, or previous regexp didn't match
256 76 50 66     653 if ( $dni_name && $dni_version || !$name ) {
      66        
257 76 50       160 $name = $dni_name if $dni_name;
258 76 100       389 $version = $dni_version if $dni_version;
259             }
260             };
261 76 50       158 warn $@ if $@;
262             }
263 115         464 return ($name, $version);
264             }
265              
266              
267 23   100 23 1 224 sub path_class_dir { $_[0]->{path_class_dir} ||= 'Path::Class::Dir' }
268 533   100 533 1 3557 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 138 my ($self, $file) = @_;
278 88         390 $self->path_class_file->new_foreign($self->file_spec, $file)
279             }
280              
281              
282             sub perl_files {
283             return
284 43     43 1 248 grep { /\.pm$/ }
  148         499  
285             $_[0]->list_files;
286             }
287              
288              
289             sub physical_directory {
290 37     37 1 907 my ($self, @files) = @_;
291              
292 37         336 require File::Temp;
293             # dir will be removed when return value goes out of scope (in caller)
294 37         352 my $dir = File::Temp->newdir();
295              
296 37         18063 return $self->extract_into($dir, @files);
297             }
298              
299              
300             sub remove_root_dir {
301 59     59 1 128 my ($self, @files) = @_;
302 59 50       173 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       422 $files[0] =~ m{^([^\\/]+)[\\/]}
308             # if not matched quit now
309             or return (undef, @files);
310              
311 32         108 my $dir = $1;
312 32         39 my @rel;
313              
314             # strip $dir from each file
315 32         74 for (@files) {
316              
317 98 50       1069 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         217 push @rel, $1;
322             }
323              
324 32         145 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 786 my ($self) = @_;
334              
335             # call list_files instead of find_files so that it caches the result
336 193 100       565 $self->list_files
337             unless exists $self->{root};
338              
339 193         759 return $self->{root};
340             }
341              
342              
343             sub set_name_and_version {
344 115     115 1 7697 my ($self, @values) = @_;
345 115         205 my @fields = qw( name version );
346              
347 115         326 foreach my $i ( 0 .. $#fields ){
348 230 100 100     994 $self->{ $fields[$i] } = $values[$i]
349             if !exists $self->{ $fields[$i] } && defined $values[$i];
350             }
351 115         259 return;
352             }
353              
354              
355             # version() defined with name()
356              
357             1;
358              
359             __END__