eaiovnaovbqoebvqoeavibavo PKnjZZvv Validator.pmnu[use 5.006; use strict; use warnings; package CPAN::Meta::Validator; our $VERSION = '2.120921'; # VERSION #--------------------------------------------------------------------------# # This code copied and adapted from Test::CPAN::Meta # by Barbie, for Miss Barbell Productions, # L #--------------------------------------------------------------------------# #--------------------------------------------------------------------------# # Specification Definitions #--------------------------------------------------------------------------# my %known_specs = ( '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html', '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html', '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html', '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html', '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html' ); my %known_urls = map {$known_specs{$_} => $_} keys %known_specs; my $module_map1 = { 'map' => { ':key' => { name => \&module, value => \&exversion } } }; my $module_map2 = { 'map' => { ':key' => { name => \&module, value => \&version } } }; my $no_index_2 = { 'map' => { file => { list => { value => \&string } }, directory => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&custom_2, value => \&anything }, } }; my $no_index_1_3 = { 'map' => { file => { list => { value => \&string } }, directory => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&string, value => \&anything }, } }; my $no_index_1_2 = { 'map' => { file => { list => { value => \&string } }, dir => { list => { value => \&string } }, 'package' => { list => { value => \&string } }, namespace => { list => { value => \&string } }, ':key' => { name => \&string, value => \&anything }, } }; my $no_index_1_1 = { 'map' => { ':key' => { name => \&string, list => { value => \&string } }, } }; my $prereq_map = { map => { ':key' => { name => \&phase, 'map' => { ':key' => { name => \&relation, %$module_map1, }, }, } }, }; my %definitions = ( '2' => { # REQUIRED 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, lazylist => { value => \&string } }, 'dynamic_config' => { mandatory => 1, value => \&boolean }, 'generated_by' => { mandatory => 1, value => \&string }, 'license' => { mandatory => 1, lazylist => { value => \&license } }, 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { value => \&url }, ':key' => { name => \&custom_2, value => \&anything }, } }, 'name' => { mandatory => 1, value => \&string }, 'release_status' => { mandatory => 1, value => \&release_status }, 'version' => { mandatory => 1, value => \&version }, # OPTIONAL 'description' => { value => \&string }, 'keywords' => { lazylist => { value => \&string } }, 'no_index' => $no_index_2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, prereqs => $prereq_map, ':key' => { name => \&custom_2, value => \&anything }, } } } }, 'prereqs' => $prereq_map, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&custom_2, value => \&anything }, } } } }, 'resources' => { 'map' => { license => { lazylist => { value => \&url } }, homepage => { value => \&url }, bugtracker => { 'map' => { web => { value => \&url }, mailto => { value => \&string}, ':key' => { name => \&custom_2, value => \&anything }, } }, repository => { 'map' => { web => { value => \&url }, url => { value => \&url }, type => { value => \&string }, ':key' => { name => \&custom_2, value => \&anything }, } }, ':key' => { value => \&string, name => \&custom_2 }, } }, # CUSTOM -- additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&custom_2, value => \&anything }, }, '1.4' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'configure_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'no_index' => $no_index_1_3, 'private' => $no_index_1_3, 'keywords' => { list => { value => \&string } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, '1.3' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'abstract' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'no_index' => $no_index_1_3, 'private' => $no_index_1_3, 'keywords' => { list => { value => \&string } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # v1.2 is misleading, it seems to assume that a number of fields where created # within v1.1, when they were created within v1.2. This may have been an # original mistake, and that a v1.1 was retro fitted into the timeline, when # v1.2 was originally slated as v1.1. But I could be wrong ;) '1.2' => { 'meta-spec' => { mandatory => 1, 'map' => { version => { mandatory => 1, value => \&version}, url => { mandatory => 1, value => \&urlspec }, ':key' => { name => \&string, value => \&anything }, }, }, 'name' => { mandatory => 1, value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { mandatory => 1, value => \&license }, 'generated_by' => { mandatory => 1, value => \&string }, 'author' => { mandatory => 1, list => { value => \&string } }, 'abstract' => { mandatory => 1, value => \&string }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'keywords' => { list => { value => \&string } }, 'private' => $no_index_1_2, '$no_index' => $no_index_1_2, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, 'optional_features' => { 'map' => { ':key' => { name => \&string, 'map' => { description => { value => \&string }, requires => $module_map1, recommends => $module_map1, build_requires => $module_map1, conflicts => $module_map2, ':key' => { name => \&string, value => \&anything }, } } } }, 'provides' => { 'map' => { ':key' => { name => \&module, 'map' => { file => { mandatory => 1, value => \&file }, version => { value => \&version }, ':key' => { name => \&string, value => \&anything }, } } } }, 'resources' => { 'map' => { license => { value => \&url }, homepage => { value => \&url }, bugtracker => { value => \&url }, repository => { value => \&url }, ':key' => { value => \&string, name => \&custom_1 }, } }, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # note that the 1.1 spec only specifies 'version' as mandatory '1.1' => { 'name' => { value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { value => \&license }, 'generated_by' => { value => \&string }, 'license_uri' => { value => \&url }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'private' => $no_index_1_1, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, # note that the 1.0 spec doesn't specify optional or mandatory fields # but we will treat version as mandatory since otherwise META 1.0 is # completely arbitrary and pointless '1.0' => { 'name' => { value => \&string }, 'version' => { mandatory => 1, value => \&version }, 'license' => { value => \&license }, 'generated_by' => { value => \&string }, 'license_uri' => { value => \&url }, 'distribution_type' => { value => \&string }, 'dynamic_config' => { value => \&boolean }, 'requires' => $module_map1, 'recommends' => $module_map1, 'build_requires' => $module_map1, 'conflicts' => $module_map2, # additional user defined key/value pairs # note we can only validate the key name, as the structure is user defined ':key' => { name => \&string, value => \&anything }, }, ); #--------------------------------------------------------------------------# # Code #--------------------------------------------------------------------------# sub new { my ($class,$data) = @_; # create an attributes hash my $self = { 'data' => $data, 'spec' => $data->{'meta-spec'}{'version'} || "1.0", 'errors' => undef, }; # create the object return bless $self, $class; } sub is_valid { my $self = shift; my $data = $self->{data}; my $spec_version = $self->{spec}; $self->check_map($definitions{$spec_version},$data); return ! $self->errors; } sub errors { my $self = shift; return () unless(defined $self->{errors}); return @{$self->{errors}}; } my $spec_error = "Missing validation action in specification. " . "Must be one of 'map', 'list', 'lazylist', or 'value'"; sub check_map { my ($self,$spec,$data) = @_; if(ref($spec) ne 'HASH') { $self->_error( "Unknown META specification, cannot validate." ); return; } if(ref($data) ne 'HASH') { $self->_error( "Expected a map structure from string or file." ); return; } for my $key (keys %$spec) { next unless($spec->{$key}->{mandatory}); next if(defined $data->{$key}); push @{$self->{stack}}, $key; $self->_error( "Missing mandatory field, '$key'" ); pop @{$self->{stack}}; } for my $key (keys %$data) { push @{$self->{stack}}, $key; if($spec->{$key}) { if($spec->{$key}{value}) { $spec->{$key}{value}->($self,$key,$data->{$key}); } elsif($spec->{$key}{'map'}) { $self->check_map($spec->{$key}{'map'},$data->{$key}); } elsif($spec->{$key}{'list'}) { $self->check_list($spec->{$key}{'list'},$data->{$key}); } elsif($spec->{$key}{'lazylist'}) { $self->check_lazylist($spec->{$key}{'lazylist'},$data->{$key}); } else { $self->_error( "$spec_error for '$key'" ); } } elsif ($spec->{':key'}) { $spec->{':key'}{name}->($self,$key,$key); if($spec->{':key'}{value}) { $spec->{':key'}{value}->($self,$key,$data->{$key}); } elsif($spec->{':key'}{'map'}) { $self->check_map($spec->{':key'}{'map'},$data->{$key}); } elsif($spec->{':key'}{'list'}) { $self->check_list($spec->{':key'}{'list'},$data->{$key}); } elsif($spec->{':key'}{'lazylist'}) { $self->check_lazylist($spec->{':key'}{'lazylist'},$data->{$key}); } else { $self->_error( "$spec_error for ':key'" ); } } else { $self->_error( "Unknown key, '$key', found in map structure" ); } pop @{$self->{stack}}; } } # if it's a string, make it into a list and check the list sub check_lazylist { my ($self,$spec,$data) = @_; if ( defined $data && ! ref($data) ) { $data = [ $data ]; } $self->check_list($spec,$data); } sub check_list { my ($self,$spec,$data) = @_; if(ref($data) ne 'ARRAY') { $self->_error( "Expected a list structure" ); return; } if(defined $spec->{mandatory}) { if(!defined $data->[0]) { $self->_error( "Missing entries from mandatory list" ); } } for my $value (@$data) { push @{$self->{stack}}, $value || ""; if(defined $spec->{value}) { $spec->{value}->($self,'list',$value); } elsif(defined $spec->{'map'}) { $self->check_map($spec->{'map'},$value); } elsif(defined $spec->{'list'}) { $self->check_list($spec->{'list'},$value); } elsif(defined $spec->{'lazylist'}) { $self->check_lazylist($spec->{'lazylist'},$value); } elsif ($spec->{':key'}) { $self->check_map($spec,$value); } else { $self->_error( "$spec_error associated with '$self->{stack}[-2]'" ); } pop @{$self->{stack}}; } } sub header { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value && $value =~ /^--- #YAML:1.0/); } $self->_error( "file does not have a valid YAML header." ); return 0; } sub release_status { my ($self,$key,$value) = @_; if(defined $value) { my $version = $self->{data}{version} || ''; if ( $version =~ /_/ ) { return 1 if ( $value =~ /\A(?:testing|unstable)\z/ ); $self->_error( "'$value' for '$key' is invalid for version '$version'" ); } else { return 1 if ( $value =~ /\A(?:stable|testing|unstable)\z/ ); $self->_error( "'$value' for '$key' is invalid" ); } } else { $self->_error( "'$key' is not defined" ); } return 0; } # _uri_split taken from URI::Split by Gisle Aas, Copyright 2003 sub _uri_split { return $_[0] =~ m,(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?,; } sub url { my ($self,$key,$value) = @_; if(defined $value) { my ($scheme, $auth, $path, $query, $frag) = _uri_split($value); unless ( defined $scheme && length $scheme ) { $self->_error( "'$value' for '$key' does not have a URL scheme" ); return 0; } unless ( defined $auth && length $auth ) { $self->_error( "'$value' for '$key' does not have a URL authority" ); return 0; } return 1; } $value ||= ''; $self->_error( "'$value' for '$key' is not a valid URL." ); return 0; } sub urlspec { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value && $known_specs{$self->{spec}} eq $value); if($value && $known_urls{$value}) { $self->_error( 'META specification URL does not match version' ); return 0; } } $self->_error( 'Unknown META specification' ); return 0; } sub anything { return 1 } sub string { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value || $value =~ /^0$/); } $self->_error( "value is an undefined string" ); return 0; } sub string_or_undef { my ($self,$key,$value) = @_; return 1 unless(defined $value); return 1 if($value || $value =~ /^0$/); $self->_error( "No string defined for '$key'" ); return 0; } sub file { my ($self,$key,$value) = @_; return 1 if(defined $value); $self->_error( "No file defined for '$key'" ); return 0; } sub exversion { my ($self,$key,$value) = @_; if(defined $value && ($value || $value =~ /0/)) { my $pass = 1; for(split(",",$value)) { $self->version($key,$_) or ($pass = 0); } return $pass; } $value = '' unless(defined $value); $self->_error( "'$value' for '$key' is not a valid version." ); return 0; } sub version { my ($self,$key,$value) = @_; if(defined $value) { return 0 unless($value || $value =~ /0/); return 1 if($value =~ /^\s*((<|<=|>=|>|!=|==)\s*)?v?\d+((\.\d+((_|\.)\d+)?)?)/); } else { $value = ''; } $self->_error( "'$value' for '$key' is not a valid version." ); return 0; } sub boolean { my ($self,$key,$value) = @_; if(defined $value) { return 1 if($value =~ /^(0|1|true|false)$/); } else { $value = ''; } $self->_error( "'$value' for '$key' is not a boolean value." ); return 0; } my %v1_licenses = ( 'perl' => 'http://dev.perl.org/licenses/', 'gpl' => 'http://www.opensource.org/licenses/gpl-license.php', 'apache' => 'http://apache.org/licenses/LICENSE-2.0', 'artistic' => 'http://opensource.org/licenses/artistic-license.php', 'artistic_2' => 'http://opensource.org/licenses/artistic-license-2.0.php', 'lgpl' => 'http://www.opensource.org/licenses/lgpl-license.php', 'bsd' => 'http://www.opensource.org/licenses/bsd-license.php', 'gpl' => 'http://www.opensource.org/licenses/gpl-license.php', 'mit' => 'http://opensource.org/licenses/mit-license.php', 'mozilla' => 'http://opensource.org/licenses/mozilla1.1.php', 'open_source' => undef, 'unrestricted' => undef, 'restrictive' => undef, 'unknown' => undef, ); my %v2_licenses = map { $_ => 1 } qw( agpl_3 apache_1_1 apache_2_0 artistic_1 artistic_2 bsd freebsd gfdl_1_2 gfdl_1_3 gpl_1 gpl_2 gpl_3 lgpl_2_1 lgpl_3_0 mit mozilla_1_0 mozilla_1_1 openssl perl_5 qpl_1_0 ssleay sun zlib open_source restricted unrestricted unknown ); sub license { my ($self,$key,$value) = @_; my $licenses = $self->{spec} < 2 ? \%v1_licenses : \%v2_licenses; if(defined $value) { return 1 if($value && exists $licenses->{$value}); } else { $value = ''; } $self->_error( "License '$value' is invalid" ); return 0; } sub custom_1 { my ($self,$key) = @_; if(defined $key) { # a valid user defined key should be alphabetic # and contain at least one capital case letter. return 1 if($key && $key =~ /^[_a-z]+$/i && $key =~ /[A-Z]/); } else { $key = ''; } $self->_error( "Custom resource '$key' must be in CamelCase." ); return 0; } sub custom_2 { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^x_/i); # user defined } else { $key = ''; } $self->_error( "Custom key '$key' must begin with 'x_' or 'X_'." ); return 0; } sub identifier { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^([a-z][_a-z]+)$/i); # spec 2.0 defined } else { $key = ''; } $self->_error( "Key '$key' is not a legal identifier." ); return 0; } sub module { my ($self,$key) = @_; if(defined $key) { return 1 if($key && $key =~ /^[A-Za-z0-9_]+(::[A-Za-z0-9_]+)*$/); } else { $key = ''; } $self->_error( "Key '$key' is not a legal module name." ); return 0; } my @valid_phases = qw/ configure build test runtime develop /; sub phase { my ($self,$key) = @_; if(defined $key) { return 1 if( length $key && grep { $key eq $_ } @valid_phases ); return 1 if $key =~ /x_/i; } else { $key = ''; } $self->_error( "Key '$key' is not a legal phase." ); return 0; } my @valid_relations = qw/ requires recommends suggests conflicts /; sub relation { my ($self,$key) = @_; if(defined $key) { return 1 if( length $key && grep { $key eq $_ } @valid_relations ); return 1 if $key =~ /x_/i; } else { $key = ''; } $self->_error( "Key '$key' is not a legal prereq relationship." ); return 0; } sub _error { my $self = shift; my $mess = shift; $mess .= ' ('.join(' -> ',@{$self->{stack}}).')' if($self->{stack}); $mess .= " [Validation: $self->{spec}]"; push @{$self->{errors}}, $mess; } 1; # ABSTRACT: validate CPAN distribution metadata structures =pod =head1 NAME CPAN::Meta::Validator - validate CPAN distribution metadata structures =head1 VERSION version 2.120921 =head1 SYNOPSIS my $struct = decode_json_file('META.json'); my $cmv = CPAN::Meta::Validator->new( $struct ); unless ( $cmv->is_valid ) { my $msg = "Invalid META structure. Errors found:\n"; $msg .= join( "\n", $cmv->errors ); die $msg; } =head1 DESCRIPTION This module validates a CPAN Meta structure against the version of the the specification claimed in the C field of the structure. =head1 METHODS =head2 new my $cmv = CPAN::Meta::Validator->new( $struct ) The constructor must be passed a metadata structure. =head2 is_valid if ( $cmv->is_valid ) { ... } Returns a boolean value indicating whether the metadata provided is valid. =head2 errors warn( join "\n", $cmv->errors ); Returns a list of errors seen during validation. =begin :internals =head2 Check Methods =over =item * check_map($spec,$data) Checks whether a map (or hash) part of the data structure conforms to the appropriate specification definition. =item * check_list($spec,$data) Checks whether a list (or array) part of the data structure conforms to the appropriate specification definition. =item * check_lazylist($spec,$data) Checks whether a list conforms, but converts strings to a single-element list =back =head2 Validator Methods =over =item * header($self,$key,$value) Validates that the header is valid. Note: No longer used as we now read the data structure, not the file. =item * url($self,$key,$value) Validates that a given value is in an acceptable URL format =item * urlspec($self,$key,$value) Validates that the URL to a META specification is a known one. =item * string_or_undef($self,$key,$value) Validates that the value is either a string or an undef value. Bit of a catchall function for parts of the data structure that are completely user defined. =item * string($self,$key,$value) Validates that a string exists for the given key. =item * file($self,$key,$value) Validate that a file is passed for the given key. This may be made more thorough in the future. For now it acts like \&string. =item * exversion($self,$key,$value) Validates a list of versions, e.g. '<= 5, >=2, ==3, !=4, >1, <6, 0'. =item * version($self,$key,$value) Validates a single version string. Versions of the type '5.8.8' and '0.00_00' are both valid. A leading 'v' like 'v1.2.3' is also valid. =item * boolean($self,$key,$value) Validates for a boolean value. Currently these values are '1', '0', 'true', 'false', however the latter 2 may be removed. =item * license($self,$key,$value) Validates that a value is given for the license. Returns 1 if an known license type, or 2 if a value is given but the license type is not a recommended one. =item * custom_1($self,$key,$value) Validates that the given key is in CamelCase, to indicate a user defined keyword and only has characters in the class [-_a-zA-Z]. In version 1.X of the spec, this was only explicitly stated for 'resources'. =item * custom_2($self,$key,$value) Validates that the given key begins with 'x_' or 'X_', to indicate a user defined keyword and only has characters in the class [-_a-zA-Z] =item * identifier($self,$key,$value) Validates that key is in an acceptable format for the META specification, for an identifier, i.e. any that matches the regular expression qr/[a-z][a-z_]/i. =item * module($self,$key,$value) Validates that a given key is in an acceptable module name format, e.g. 'Test::CPAN::Meta::Version'. =back =end :internals =for Pod::Coverage anything boolean check_lazylist check_list custom_1 custom_2 exversion file identifier license module phase relation release_status string string_or_undef url urlspec version header check_map =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ PKnjZ88Spec.pmnu[# vi:tw=72 use 5.006; use strict; use warnings; package CPAN::Meta::Spec; our $VERSION = '2.120921'; # VERSION 1; # ABSTRACT: specification for CPAN distribution metadata __END__ =pod =head1 NAME CPAN::Meta::Spec - specification for CPAN distribution metadata =head1 VERSION version 2.120921 =head1 SYNOPSIS my $distmeta = { name => 'Module-Build', abstract => 'Build and install Perl modules', description => "Module::Build is a system for " . "building, testing, and installing Perl modules. " . "It is meant to ... blah blah blah ...", version => '0.36', release_status => 'stable', author => [ 'Ken Williams ', 'Module-Build List ', # additional contact ], license => [ 'perl_5' ], prereqs => { runtime => { requires => { 'perl' => '5.006', 'ExtUtils::Install' => '0', 'File::Basename' => '0', 'File::Compare' => '0', 'IO::File' => '0', }, recommends => { 'Archive::Tar' => '1.00', 'ExtUtils::Install' => '0.3', 'ExtUtils::ParseXS' => '2.02', }, }, build => { requires => { 'Test::More' => '0', }, } }, resources => { license => ['http://dev.perl.org/licenses/'], }, optional_features => { domination => { description => 'Take over the world', prereqs => { develop => { requires => { 'Genius::Evil' => '1.234' } }, runtime => { requires => { 'Machine::Weather' => '2.0' } }, }, }, }, dynamic_config => 1, keywords => [ qw/ toolchain cpan dual-life / ], 'meta-spec' => { version => '2', url => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', }, generated_by => 'Module::Build version 0.36', }; =head1 DESCRIPTION This document describes version 2 of the CPAN distribution metadata specification, also known as the "CPAN Meta Spec". Revisions of this specification for typo corrections and prose clarifications may be issued as CPAN::Meta::Spec 2.I. These revisions will never change semantics or add or remove specified behavior. Distribution metadata describe important properties of Perl distributions. Distribution building tools like Module::Build, Module::Install, ExtUtils::MakeMaker or Dist::Zilla should create a metadata file in accordance with this specification and include it with the distribution for use by automated tools that index, examine, package or install Perl distributions. =head1 TERMINOLOGY =over 4 =item distribution This is the primary object described by the metadata. In the context of this document it usually refers to a collection of modules, scripts, and/or documents that are distributed together for other developers to use. Examples of distributions are C, C, or C. =item module This refers to a reusable library of code contained in a single file. Modules usually contain one or more packages and are often referred to by the name of a primary package that can be mapped to the file name. For example, one might refer to C instead of F =item package This refers to a namespace declared with the Perl C statement. In Perl, packages often have a version number property given by the C<$VERSION> variable in the namespace. =item consumer This refers to code that reads a metadata file, deserializes it into a data structure in memory, or interprets a data structure of metadata elements. =item producer This refers to code that constructs a metadata data structure, serializes into a bytestream and/or writes it to disk. =item must, should, may, etc. These terms are interpreted as described in IETF RFC 2119. =back =head1 DATA TYPES Fields in the L section describe data elements, each of which has an associated data type as described herein. There are four primitive types: Boolean, String, List and Map. Other types are subtypes of primitives and define compound data structures or define constraints on the values of a data element. =head2 Boolean A I is used to provide a true or false value. It B be represented as a defined value. =head2 String A I is data element containing a non-zero length sequence of Unicode characters, such as an ordinary Perl scalar that is not a reference. =head2 List A I is an ordered collection of zero or more data elements. Elements of a List may be of mixed types. Producers B represent List elements using a data structure which unambiguously indicates that multiple values are possible, such as a reference to a Perl array (an "arrayref"). Consumers expecting a List B consider a String as equivalent to a List of length 1. =head2 Map A I is an unordered collection of zero or more data elements ("values"), indexed by associated String elements ("keys"). The Map's value elements may be of mixed types. =head2 License String A I is a subtype of String with a restricted set of values. Valid values are described in detail in the description of the L field. =head2 URL I is a subtype of String containing a Uniform Resource Locator or Identifier. [ This type is called URL and not URI for historical reasons. ] =head2 Version A I is a subtype of String containing a value that describes the version number of packages or distributions. Restrictions on format are described in detail in the L section. =head2 Version Range The I type is a subtype of String. It describes a range of Versions that may be present or installed to fulfill prerequisites. It is specified in detail in the L section. =head1 STRUCTURE The metadata structure is a data element of type Map. This section describes valid keys within the Map. Any keys not described in this specification document (whether top-level or within compound data structures described herein) are considered I and B begin with an "x" or "X" and be followed by an underscore; i.e. they must match the pattern: C<< qr{\Ax_}i >>. If a custom key refers to a compound data structure, subkeys within it do not need an "x_" or "X_" prefix. Consumers of metadata may ignore any or all custom keys. All other keys not described herein are invalid and should be ignored by consumers. Producers must not generate or output invalid keys. For each key, an example is provided followed by a description. The description begins with the version of spec in which the key was added or in which the definition was modified, whether the key is I or I and the data type of the corresponding data element. These items are in parentheses, brackets and braces, respectively. If a data type is a Map or Map subtype, valid subkeys will be described as well. Some fields are marked I. These are shown for historical context and must not be produced in or consumed from any metadata structure of version 2 or higher. =head2 REQUIRED FIELDS =head3 abstract Example: abstract => 'Build and install Perl modules' (Spec 1.2) [required] {String} This is a short description of the purpose of the distribution. =head3 author Example: author => [ 'Ken Williams ' ] (Spec 1.2) [required] {List of one or more Strings} This List indicates the person(s) to contact concerning the distribution. The preferred form of the contact string is: contact-name This field provides a general contact list independent of other structured fields provided within the L field, such as C. The addressee(s) can be contacted for any purpose including but not limited to (security) problems with the distribution, questions about the distribution or bugs in the distribution. A distribution's original author is usually the contact listed within this field. Co-maintainers, successor maintainers or mailing lists devoted to the distribution may also be listed in addition to or instead of the original author. =head3 dynamic_config Example: dynamic_config => 1 (Spec 2) [required] {Boolean} A boolean flag indicating whether a F or F (or similar) must be executed to determine prerequisites. This field should be set to a true value if the distribution performs some dynamic configuration (asking questions, sensing the environment, etc.) as part of its configuration. This field should be set to a false value to indicate that prerequisites included in metadata may be considered final and valid for static analysis. This field explicitly B indicate whether installation may be safely performed without using a Makefile or Build file, as there may be special files to install or custom installation targets (e.g. for dual-life modules that exist on CPAN as well as in the Perl core). This field only defines whether prerequisites are complete as given in the metadata. =head3 generated_by Example: generated_by => 'Module::Build version 0.36' (Spec 1.0) [required] {String} This field indicates the tool that was used to create this metadata. There are no defined semantics for this field, but it is traditional to use a string in the form "Generating::Package version 1.23" or the author's name, if the file was generated by hand. =head3 license Example: license => [ 'perl_5' ] license => [ 'apache_2', 'mozilla_1_0' ] (Spec 2) [required] {List of one or more License Strings} One or more licenses that apply to some or all of the files in the distribution. If multiple licenses are listed, the distribution documentation should be consulted to clarify the interpretation of multiple licenses. The following list of license strings are valid: string description ------------- ----------------------------------------------- agpl_3 GNU Affero General Public License, Version 3 apache_1_1 Apache Software License, Version 1.1 apache_2_0 Apache License, Version 2.0 artistic_1 Artistic License, (Version 1) artistic_2 Artistic License, Version 2.0 bsd BSD License (three-clause) freebsd FreeBSD License (two-clause) gfdl_1_2 GNU Free Documentation License, Version 1.2 gfdl_1_3 GNU Free Documentation License, Version 1.3 gpl_1 GNU General Public License, Version 1 gpl_2 GNU General Public License, Version 2 gpl_3 GNU General Public License, Version 3 lgpl_2_1 GNU Lesser General Public License, Version 2.1 lgpl_3_0 GNU Lesser General Public License, Version 3.0 mit MIT (aka X11) License mozilla_1_0 Mozilla Public License, Version 1.0 mozilla_1_1 Mozilla Public License, Version 1.1 openssl OpenSSL License perl_5 The Perl 5 License (Artistic 1 & GPL 1 or later) qpl_1_0 Q Public License, Version 1.0 ssleay Original SSLeay License sun Sun Internet Standards Source License (SISSL) zlib zlib License The following license strings are also valid and indicate other licensing not described above: string description ------------- ----------------------------------------------- open_source Other Open Source Initiative (OSI) approved license restricted Requires special permission from copyright holder unrestricted Not an OSI approved license, but not restricted unknown License not provided in metadata All other strings are invalid in the license field. =head3 meta-spec Example: 'meta-spec' => { version => '2', url => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', } (Spec 1.2) [required] {Map} This field indicates the version of the CPAN Meta Spec that should be used to interpret the metadata. Consumers must check this key as soon as possible and abort further metadata processing if the meta-spec version is not supported by the consumer. The following keys are valid, but only C is required. =over =item version This subkey gives the integer I of the CPAN Meta Spec against which the document was generated. =item url This is a I of the metadata specification document corresponding to the given version. This is strictly for human-consumption and should not impact the interpretation of the document. =back =head3 name Example: name => 'Module-Build' (Spec 1.0) [required] {String} This field is the name of the distribution. This is often created by taking the "main package" in the distribution and changing C<::> to C<->, but the name may be completely unrelated to the packages within the distribution. C.f. L. =head3 release_status Example: release_status => 'stable' (Spec 2) [required] {String} This field provides the release status of this distribution. If the C field contains an underscore character, then C B be "stable." The C field B have one of the following values: =over =item stable This indicates an ordinary, "final" release that should be indexed by PAUSE or other indexers. =item testing This indicates a "beta" release that is substantially complete, but has an elevated risk of bugs and requires additional testing. The distribution should not be installed over a stable release without an explicit request or other confirmation from a user. This release status may also be used for "release candidate" versions of a distribution. =item unstable This indicates an "alpha" release that is under active development, but has been released for early feedback or testing and may be missing features or may have serious bugs. The distribution should not be installed over a stable release without an explicit request or other confirmation from a user. =back Consumers B use this field to determine how to index the distribution for CPAN or other repositories in addition to or in replacement of heuristics based on version number or file name. =head3 version Example: version => '0.36' (Spec 1.0) [required] {Version} This field gives the version of the distribution to which the metadata structure refers. =head2 OPTIONAL FIELDS =head3 description Example: description => "Module::Build is a system for " . "building, testing, and installing Perl modules. " . "It is meant to ... blah blah blah ...", (Spec 2) [optional] {String} A longer, more complete description of the purpose or intended use of the distribution than the one provided by the C key. =head3 keywords Example: keywords => [ qw/ toolchain cpan dual-life / ] (Spec 1.1) [optional] {List of zero or more Strings} A List of keywords that describe this distribution. Keywords B include whitespace. =head3 no_index Example: no_index => { file => [ 'My/Module.pm' ], directory => [ 'My/Private' ], package => [ 'My::Module::Secret' ], namespace => [ 'My::Module::Sample' ], } (Spec 1.2) [optional] {Map} This Map describes any files, directories, packages, and namespaces that are private to the packaging or implementation of the distribution and should be ignored by indexing or search tools. Valid subkeys are as follows: =over =item file A I of relative paths to files. Paths B specified with unix conventions. =item directory A I of relative paths to directories. Paths B specified with unix conventions. [ Note: previous editions of the spec had C instead of C ] =item package A I of package names. =item namespace A I of package namespaces, where anything below the namespace must be ignored, but I the namespace itself. In the example above for C, C would be ignored, but C would not. =back =head3 optional_features Example: optional_features => { sqlite => { description => 'Provides SQLite support', prereqs => { runtime => { requires => { 'DBD::SQLite' => '1.25' } } } } } (Spec 2) [optional] {Map} This Map describes optional features with incremental prerequisites. Each key of the C Map is a String used to identify the feature and each value is a Map with additional information about the feature. Valid subkeys include: =over =item description This is a String describing the feature. Every optional feature should provide a description =item prereqs This entry is required and has the same structure as that of the C> key. It provides a list of package requirements that must be satisfied for the feature to be supported or enabled. There is one crucial restriction: the prereqs of an optional feature B include C phase prereqs. =back Consumers B include optional features as prerequisites without explicit instruction from users (whether via interactive prompting, a function parameter or a configuration value, etc. ). If an optional feature is used by a consumer to add additional prerequisites, the consumer should merge the optional feature prerequisites into those given by the C key using the same semantics. See L for details on merging prerequisites. I Because there is currently no way for a distribution to specify a dependency on an optional feature of another dependency, the use of C is discouraged. Instead, create a separate, installable distribution that ensures the desired feature is available. For example, if C has a "Baz" feature, release a separate C distribution that satisfies requirements for the feature. =head3 prereqs Example: prereqs => { runtime => { requires => { 'perl' => '5.006', 'File::Spec' => '0.86', 'JSON' => '2.16', }, recommends => { 'JSON::XS' => '2.26', }, suggests => { 'Archive::Tar' => '0', }, }, build => { requires => { 'Alien::SDL' => '1.00', }, }, test => { recommends => { 'Test::Deep' => '0.10', }, } } (Spec 2) [optional] {Map} This is a Map that describes all the prerequisites of the distribution. The keys are phases of activity, such as C, C, C or C. Values are Maps in which the keys name the type of prerequisite relationship such as C, C, or C and the value provides a set of prerequisite relations. The set of relations B be specified as a Map of package names to version ranges. The full definition for this field is given in the L section. =head3 provides Example: provides => { 'Foo::Bar' => { file => 'lib/Foo/Bar.pm', version => 0.27_02 }, 'Foo::Bar::Blah' => { file => 'lib/Foo/Bar/Blah.pm', }, 'Foo::Bar::Baz' => { file => 'lib/Foo/Bar/Baz.pm', version => 0.3, }, } (Spec 1.2) [optional] {Map} This describes all packages provided by this distribution. This information is used by distribution and automation mechanisms like PAUSE, CPAN, and search.cpan.org to build indexes saying in which distribution various packages can be found. The keys of C are package names that can be found within the distribution. The values are Maps with the following valid subkeys: =over =item file This field is required. The value must contain a Unix-style relative file path from the root of the distribution to the module containing the package. =item version This field contains a I String for the package, if one exists. =back =head3 resources Example: resources => { license => [ 'http://dev.perl.org/licenses/' ], homepage => 'http://sourceforge.net/projects/module-build', bugtracker => { web => 'http://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Meta', mailto => 'meta-bugs@example.com', }, repository => { url => 'git://github.com/dagolden/cpan-meta.git', web => 'http://github.com/dagolden/cpan-meta', type => 'git', }, x_twitter => 'http://twitter.com/cpan_linked/', } (Spec 2) [optional] {Map} This field describes resources related to this distribution. Valid subkeys include: =over =item homepage The official home of this project on the web. =item license A List of I's that relate to this distribution's license. As with the top-level C field, distribution documentation should be consulted to clarify the interpretation of multiple licenses provided here. =item bugtracker This entry describes the bug tracking system for this distribution. It is a Map with the following valid keys: web - a URL pointing to a web front-end for the bug tracker mailto - an email address to which bugs can be sent =item repository This entry describes the source control repository for this distribution. It is a Map with the following valid keys: url - a URL pointing to the repository itself web - a URL pointing to a web front-end for the repository type - a lowercase string indicating the VCS used Because a url like C is ambiguous as to type, producers should provide a C whenever a C key is given. The C field should be the name of the most common program used to work with the repository, e.g. git, svn, cvs, darcs, bzr or hg. =back =head2 DEPRECATED FIELDS =head3 build_requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 configure_requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 conflicts I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 distribution_type I<(Deprecated in Spec 2)> [optional] {String} This field indicated 'module' or 'script' but was considered meaningless, since many distributions are hybrids of several kinds of things. =head3 license_uri I<(Deprecated in Spec 1.2)> [optional] {URL} Replaced by C in C =head3 private I<(Deprecated in Spec 1.2)> [optional] {Map} This field has been renamed to L. =head3 recommends I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head3 requires I<(Deprecated in Spec 2)> [optional] {String} Replaced by C =head1 VERSION NUMBERS =head2 Version Formats This section defines the Version type, used by several fields in the CPAN Meta Spec. Version numbers must be treated as strings, not numbers. For example, C<1.200> B be serialized as C<1.2>. Version comparison should be delegated to the Perl L module, version 0.80 or newer. Unless otherwise specified, version numbers B appear in one of two formats: =over =item Decimal versions Decimal versions are regular "decimal numbers", with some limitations. They B be non-negative and B begin and end with a digit. A single underscore B be included, but B be between two digits. They B use exponential notation ("1.23e-2"). version => '1.234' # OK version => '1.23_04' # OK version => '1.23_04_05' # Illegal version => '1.' # Illegal version => '.1' # Illegal =item Dotted-integer versions Dotted-integer (also known as dotted-decimal) versions consist of positive integers separated by full stop characters (i.e. "dots", "periods" or "decimal points"). This are equivalent in format to Perl "v-strings", with some additional restrictions on form. They must be given in "normal" form, which has a leading "v" character and at least three integer components. To retain a one-to-one mapping with decimal versions, all components after the first B be restricted to the range 0 to 999. The final component B be separated by an underscore character instead of a period. version => 'v1.2.3' # OK version => 'v1.2_3' # OK version => 'v1.2.3.4' # OK version => 'v1.2.3_4' # OK version => 'v2009.10.31' # OK version => 'v1.2' # Illegal version => '1.2.3' # Illegal version => 'v1.2_3_4' # Illegal version => 'v1.2009.10.31' # Not recommended =back =head2 Version Ranges Some fields (prereq, optional_features) indicate the particular version(s) of some other module that may be required as a prerequisite. This section details the Version Range type used to provide this information. The simplest format for a Version Range is just the version number itself, e.g. C<2.4>. This means that B version 2.4 must be present. To indicate that B version of a prerequisite is okay, even if the prerequisite doesn't define a version at all, use the version C<0>. Alternatively, a version range B use the operators E (less than), E= (less than or equal), E (greater than), E= (greater than or equal), == (equal), and != (not equal). For example, the specification C 2.0> means that any version of the prerequisite less than 2.0 is suitable. For more complicated situations, version specifications B be AND-ed together using commas. The specification C= 1.2, != 1.5, E 2.0> indicates a version that must be B 1.2, B 2.0, and B 1.5. =head1 PREREQUISITES =head2 Prereq Spec The C key in the top-level metadata and within C define the relationship between a distribution and other packages. The prereq spec structure is a hierarchical data structure which divides prerequisites into I of activity in the installation process and I that indicate how prerequisites should be resolved. For example, to specify that C is C during the C phase, this entry would appear in the distribution metadata: prereqs => { test => { requires => { 'Data::Dumper' => '2.00' } } } =head3 Phases Requirements for regular use must be listed in the C phase. Other requirements should be listed in the earliest stage in which they are required and consumers must accumulate and satisfy requirements across phases before executing the activity. For example, C requirements must also be available during the C phase. before action requirements that must be met ---------------- -------------------------------- perl Build.PL configure perl Makefile.PL make configure, runtime, build Build make test configure, runtime, build, test Build test Consumers that install the distribution must ensure that I requirements are also installed and may install dependencies from other phases. after action requirements that must be met ---------------- -------------------------------- make install runtime Build install =over =item configure The configure phase occurs before any dynamic configuration has been attempted. Libraries required by the configure phase B be available for use before the distribution building tool has been executed. =item build The build phase is when the distribution's source code is compiled (if necessary) and otherwise made ready for installation. =item test The test phase is when the distribution's automated test suite is run. Any library that is needed only for testing and not for subsequent use should be listed here. =item runtime The runtime phase refers not only to when the distribution's contents are installed, but also to its continued use. Any library that is a prerequisite for regular use of this distribution should be indicated here. =item develop The develop phase's prereqs are libraries needed to work on the distribution's source code as its author does. These tools might be needed to build a release tarball, to run author-only tests, or to perform other tasks related to developing new versions of the distribution. =back =head3 Relationships =over =item requires These dependencies B be installed for proper completion of the phase. =item recommends Recommended dependencies are I encouraged and should be satisfied except in resource constrained environments. =item suggests These dependencies are optional, but are suggested for enhanced operation of the described distribution. =item conflicts These libraries cannot be installed when the phase is in operation. This is a very rare situation, and the C relationship should be used with great caution, or not at all. =back =head2 Merging and Resolving Prerequisites Whenever metadata consumers merge prerequisites, either from different phases or from C, they should merged in a way which preserves the intended semantics of the prerequisite structure. Generally, this means concatenating the version specifications using commas, as described in the L section. Another subtle error that can occur in resolving prerequisites comes from the way that modules in prerequisites are indexed to distribution files on CPAN. When a module is deleted from a distribution, prerequisites calling for that module could indicate an older distribution should installed, potentially overwriting files from a newer distribution. For example, as of Oct 31, 2009, the CPAN index file contained these module-distribution mappings: Class::MOP 0.94 D/DR/DROLSKY/Class-MOP-0.94.tar.gz Class::MOP::Class 0.94 D/DR/DROLSKY/Class-MOP-0.94.tar.gz Class::MOP::Class::Immutable 0.04 S/ST/STEVAN/Class-MOP-0.36.tar.gz Consider the case where "Class::MOP" 0.94 is installed. If a distribution specified "Class::MOP::Class::Immutable" as a prerequisite, it could result in Class-MOP-0.36.tar.gz being installed, overwriting any files from Class-MOP-0.94.tar.gz. Consumers of metadata B test whether prerequisites would result in installed module files being "downgraded" to an older version and B warn users or ignore the prerequisite that would cause such a result. =head1 SERIALIZATION Distribution metadata should be serialized (as a hashref) as JSON-encoded data and packaged with distributions as the file F. In the past, the distribution metadata structure had been packed with distributions as F, a file in the YAML Tiny format (for which, see L). Tools that consume distribution metadata from disk should be capable of loading F, but should prefer F if both are found. =head1 NOTES FOR IMPLEMENTORS =head2 Extracting Version Numbers from Perl Modules To get the version number from a Perl module, consumers should use the C<< MM->parse_version($file) >> method provided by L or L. For example, for the module given by C<$mod>, the version may be retrieved in one of the following ways: # via ExtUtils::MakeMaker my $file = MM->_installed_file_for_module($mod); my $version = MM->parse_version($file) The private C<_installed_file_for_module> method may be replaced with other methods for locating a module in C<@INC>. # via Module::Metadata my $info = Module::Metadata->new_from_module($mod); my $version = $info->version; If only a filename is available, the following approach may be used: # via Module::Build my $info = Module::Metadata->new_from_file($file); my $version = $info->version; =head2 Comparing Version Numbers The L module provides the most reliable way to compare version numbers in all the various ways they might be provided or might exist within modules. Given two strings containing version numbers, C<$v1> and C<$v2>, they should be converted to C objects before using ordinary comparison operators. For example: use version; if ( version->new($v1) <=> version->new($v2) ) { print "Versions are not equal\n"; } If the only comparison needed is whether an installed module is of a sufficiently high version, a direct test may be done using the string form of C and the C function. For example, for module C<$mod> and version prerequisite C<$prereq>: if ( eval "use $mod $prereq (); 1" ) { print "Module $mod version is OK.\n"; } If the values of C<$mod> and C<$prereq> have not been scrubbed, however, this presents security implications. =head1 SEE ALSO CPAN, L CPAN.pm, L CPANPLUS, L ExtUtils::MakeMaker, L Module::Build, L Module::Install, L JSON, L YAML, L =head1 CONTRIBUTORS Ken Williams wrote the original CPAN Meta Spec (also known as the "META.yml spec") in 2003 and maintained it through several revisions with input from various members of the community. In 2005, Randy Sims redrafted it from HTML to POD for the version 1.2 release. Ken continued to maintain the spec through version 1.4. In late 2009, David Golden organized the version 2 proposal review process. David and Ricardo Signes drafted the final version 2 spec in April 2010 based on the version 1.4 spec and patches contributed during the proposal process. Several others have contributed patches over the years. The full list of contributors in the repository history currently includes: 2shortplanks Avar Arnfjord Bjarmason Christopher J. Madsen Damyan Ivanov David Golden Eric Wilhelm Ken Williams Lars DIECKOW Michael G. Schwern Randy Sims Ricardo Signes =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut PKnjZV2(2E2ERequirements.pmnu[use strict; use warnings; package CPAN::Meta::Requirements; our $VERSION = '2.122'; # VERSION # ABSTRACT: a set of version requirements for a CPAN dist use Carp (); use Scalar::Util (); use version 0.77 (); # the ->parse method my @valid_options = qw( bad_version_hook ); sub new { my ($class, $options) = @_; $options ||= {}; Carp::croak "Argument to $class\->new() must be a hash reference" unless ref $options eq 'HASH'; my %self = map {; $_ => $options->{$_}} @valid_options; return bless \%self => $class; } sub _version_object { my ($self, $version) = @_; my $vobj; eval { $vobj = (! defined $version) ? version->parse(0) : (! Scalar::Util::blessed($version)) ? version->parse($version) : $version; }; if ( my $err = $@ ) { my $hook = $self->{bad_version_hook}; $vobj = eval { $hook->($version) } if ref $hook eq 'CODE'; unless (Scalar::Util::blessed($vobj) && $vobj->isa("version")) { $err =~ s{ at .* line \d+.*$}{}; die "Can't convert '$version': $err"; } } # ensure no leading '.' if ( $vobj =~ m{\A\.} ) { $vobj = version->parse("0$vobj"); } # ensure normal v-string form if ( $vobj->is_qv ) { $vobj = version->parse($vobj->normal); } return $vobj; } BEGIN { for my $type (qw(minimum maximum exclusion exact_version)) { my $method = "with_$type"; my $to_add = $type eq 'exact_version' ? $type : "add_$type"; my $code = sub { my ($self, $name, $version) = @_; $version = $self->_version_object( $version ); $self->__modify_entry_for($name, $method, $version); return $self; }; no strict 'refs'; *$to_add = $code; } } sub add_requirements { my ($self, $req) = @_; for my $module ($req->required_modules) { my $modifiers = $req->__entry_for($module)->as_modifiers; for my $modifier (@$modifiers) { my ($method, @args) = @$modifier; $self->$method($module => @args); }; } return $self; } sub accepts_module { my ($self, $module, $version) = @_; $version = $self->_version_object( $version ); return 1 unless my $range = $self->__entry_for($module); return $range->_accepts($version); } sub clear_requirement { my ($self, $module) = @_; return $self unless $self->__entry_for($module); Carp::confess("can't clear requirements on finalized requirements") if $self->is_finalized; delete $self->{requirements}{ $module }; return $self; } sub requirements_for_module { my ($self, $module) = @_; my $entry = $self->__entry_for($module); return unless $entry; return $entry->as_string; } sub required_modules { keys %{ $_[0]{requirements} } } sub clone { my ($self) = @_; my $new = (ref $self)->new; return $new->add_requirements($self); } sub __entry_for { $_[0]{requirements}{ $_[1] } } sub __modify_entry_for { my ($self, $name, $method, $version) = @_; my $fin = $self->is_finalized; my $old = $self->__entry_for($name); Carp::confess("can't add new requirements to finalized requirements") if $fin and not $old; my $new = ($old || 'CPAN::Meta::Requirements::_Range::Range') ->$method($version); Carp::confess("can't modify finalized requirements") if $fin and $old->as_string ne $new->as_string; $self->{requirements}{ $name } = $new; } sub is_simple { my ($self) = @_; for my $module ($self->required_modules) { # XXX: This is a complete hack, but also entirely correct. return if $self->__entry_for($module)->as_string =~ /\s/; } return 1; } sub is_finalized { $_[0]{finalized} } sub finalize { $_[0]{finalized} = 1 } sub as_string_hash { my ($self) = @_; my %hash = map {; $_ => $self->{requirements}{$_}->as_string } $self->required_modules; return \%hash; } my %methods_for_op = ( '==' => [ qw(exact_version) ], '!=' => [ qw(add_exclusion) ], '>=' => [ qw(add_minimum) ], '<=' => [ qw(add_maximum) ], '>' => [ qw(add_minimum add_exclusion) ], '<' => [ qw(add_maximum add_exclusion) ], ); sub add_string_requirement { my ($self, $module, $req) = @_; Carp::confess("No requirement string provided for $module") unless defined $req && length $req; my @parts = split qr{\s*,\s*}, $req; for my $part (@parts) { my ($op, $ver) = $part =~ m{\A\s*(==|>=|>|<=|<|!=)\s*(.*)\z}; if (! defined $op) { $self->add_minimum($module => $part); } else { Carp::confess("illegal requirement string: $req") unless my $methods = $methods_for_op{ $op }; $self->$_($module => $ver) for @$methods; } } } sub from_string_hash { my ($class, $hash) = @_; my $self = $class->new; for my $module (keys %$hash) { my $req = $hash->{$module}; unless ( defined $req && length $req ) { $req = 0; Carp::carp("Undefined requirement for $module treated as '0'"); } $self->add_string_requirement($module, $req); } return $self; } ############################################################## { package CPAN::Meta::Requirements::_Range::Exact; sub _new { bless { version => $_[1] } => $_[0] } sub _accepts { return $_[0]{version} == $_[1] } sub as_string { return "== $_[0]{version}" } sub as_modifiers { return [ [ exact_version => $_[0]{version} ] ] } sub _clone { (ref $_[0])->_new( version->new( $_[0]{version} ) ) } sub with_exact_version { my ($self, $version) = @_; return $self->_clone if $self->_accepts($version); Carp::confess("illegal requirements: unequal exact version specified"); } sub with_minimum { my ($self, $minimum) = @_; return $self->_clone if $self->{version} >= $minimum; Carp::confess("illegal requirements: minimum above exact specification"); } sub with_maximum { my ($self, $maximum) = @_; return $self->_clone if $self->{version} <= $maximum; Carp::confess("illegal requirements: maximum below exact specification"); } sub with_exclusion { my ($self, $exclusion) = @_; return $self->_clone unless $exclusion == $self->{version}; Carp::confess("illegal requirements: excluded exact specification"); } } ############################################################## { package CPAN::Meta::Requirements::_Range::Range; sub _self { ref($_[0]) ? $_[0] : (bless { } => $_[0]) } sub _clone { return (bless { } => $_[0]) unless ref $_[0]; my ($s) = @_; my %guts = ( (exists $s->{minimum} ? (minimum => version->new($s->{minimum})) : ()), (exists $s->{maximum} ? (maximum => version->new($s->{maximum})) : ()), (exists $s->{exclusions} ? (exclusions => [ map { version->new($_) } @{ $s->{exclusions} } ]) : ()), ); bless \%guts => ref($s); } sub as_modifiers { my ($self) = @_; my @mods; push @mods, [ add_minimum => $self->{minimum} ] if exists $self->{minimum}; push @mods, [ add_maximum => $self->{maximum} ] if exists $self->{maximum}; push @mods, map {; [ add_exclusion => $_ ] } @{$self->{exclusions} || []}; return \@mods; } sub as_string { my ($self) = @_; return 0 if ! keys %$self; return "$self->{minimum}" if (keys %$self) == 1 and exists $self->{minimum}; my @exclusions = @{ $self->{exclusions} || [] }; my @parts; for my $pair ( [ qw( >= > minimum ) ], [ qw( <= < maximum ) ], ) { my ($op, $e_op, $k) = @$pair; if (exists $self->{$k}) { my @new_exclusions = grep { $_ != $self->{ $k } } @exclusions; if (@new_exclusions == @exclusions) { push @parts, "$op $self->{ $k }"; } else { push @parts, "$e_op $self->{ $k }"; @exclusions = @new_exclusions; } } } push @parts, map {; "!= $_" } @exclusions; return join q{, }, @parts; } sub with_exact_version { my ($self, $version) = @_; $self = $self->_clone; Carp::confess("illegal requirements: exact specification outside of range") unless $self->_accepts($version); return CPAN::Meta::Requirements::_Range::Exact->_new($version); } sub _simplify { my ($self) = @_; if (defined $self->{minimum} and defined $self->{maximum}) { if ($self->{minimum} == $self->{maximum}) { Carp::confess("illegal requirements: excluded all values") if grep { $_ == $self->{minimum} } @{ $self->{exclusions} || [] }; return CPAN::Meta::Requirements::_Range::Exact->_new($self->{minimum}) } Carp::confess("illegal requirements: minimum exceeds maximum") if $self->{minimum} > $self->{maximum}; } # eliminate irrelevant exclusions if ($self->{exclusions}) { my %seen; @{ $self->{exclusions} } = grep { (! defined $self->{minimum} or $_ >= $self->{minimum}) and (! defined $self->{maximum} or $_ <= $self->{maximum}) and ! $seen{$_}++ } @{ $self->{exclusions} }; } return $self; } sub with_minimum { my ($self, $minimum) = @_; $self = $self->_clone; if (defined (my $old_min = $self->{minimum})) { $self->{minimum} = (sort { $b cmp $a } ($minimum, $old_min))[0]; } else { $self->{minimum} = $minimum; } return $self->_simplify; } sub with_maximum { my ($self, $maximum) = @_; $self = $self->_clone; if (defined (my $old_max = $self->{maximum})) { $self->{maximum} = (sort { $a cmp $b } ($maximum, $old_max))[0]; } else { $self->{maximum} = $maximum; } return $self->_simplify; } sub with_exclusion { my ($self, $exclusion) = @_; $self = $self->_clone; push @{ $self->{exclusions} ||= [] }, $exclusion; return $self->_simplify; } sub _accepts { my ($self, $version) = @_; return if defined $self->{minimum} and $version < $self->{minimum}; return if defined $self->{maximum} and $version > $self->{maximum}; return if defined $self->{exclusions} and grep { $version == $_ } @{ $self->{exclusions} }; return 1; } } 1; # vim: ts=2 sts=2 sw=2 et: __END__ =pod =head1 NAME CPAN::Meta::Requirements - a set of version requirements for a CPAN dist =head1 VERSION version 2.122 =head1 SYNOPSIS use CPAN::Meta::Requirements; my $build_requires = CPAN::Meta::Requirements->new; $build_requires->add_minimum('Library::Foo' => 1.208); $build_requires->add_minimum('Library::Foo' => 2.602); $build_requires->add_minimum('Module::Bar' => 'v1.2.3'); $METAyml->{build_requires} = $build_requires->as_string_hash; =head1 DESCRIPTION A CPAN::Meta::Requirements object models a set of version constraints like those specified in the F or F files in CPAN distributions. It can be built up by adding more and more constraints, and it will reduce them to the simplest representation. Logically impossible constraints will be identified immediately by thrown exceptions. =head1 METHODS =head2 new my $req = CPAN::Meta::Requirements->new; This returns a new CPAN::Meta::Requirements object. It takes an optional hash reference argument. The following keys are supported: =over 4 =item * -- if provided, when a version cannot be parsed into a version object, this code reference will be called with the invalid version string as an argument. It must return a valid version object. =back All other keys are ignored. =head2 add_minimum $req->add_minimum( $module => $version ); This adds a new minimum version requirement. If the new requirement is redundant to the existing specification, this has no effect. Minimum requirements are inclusive. C<$version> is required, along with any greater version number. This method returns the requirements object. =head2 add_maximum $req->add_maximum( $module => $version ); This adds a new maximum version requirement. If the new requirement is redundant to the existing specification, this has no effect. Maximum requirements are inclusive. No version strictly greater than the given version is allowed. This method returns the requirements object. =head2 add_exclusion $req->add_exclusion( $module => $version ); This adds a new excluded version. For example, you might use these three method calls: $req->add_minimum( $module => '1.00' ); $req->add_maximum( $module => '1.82' ); $req->add_exclusion( $module => '1.75' ); Any version between 1.00 and 1.82 inclusive would be acceptable, except for 1.75. This method returns the requirements object. =head2 exact_version $req->exact_version( $module => $version ); This sets the version required for the given module to I the given version. No other version would be considered acceptable. This method returns the requirements object. =head2 add_requirements $req->add_requirements( $another_req_object ); This method adds all the requirements in the given CPAN::Meta::Requirements object to the requirements object on which it was called. If there are any conflicts, an exception is thrown. This method returns the requirements object. =head2 accepts_module my $bool = $req->accepts_modules($module => $version); Given an module and version, this method returns true if the version specification for the module accepts the provided version. In other words, given: Module => '>= 1.00, < 2.00' We will accept 1.00 and 1.75 but not 0.50 or 2.00. For modules that do not appear in the requirements, this method will return true. =head2 clear_requirement $req->clear_requirement( $module ); This removes the requirement for a given module from the object. This method returns the requirements object. =head2 requirements_for_module $req->requirements_for_module( $module ); This returns a string containing the version requirements for a given module in the format described in L or undef if the given module has no requirements. This should only be used for informational purposes such as error messages and should not be interpreted or used for comparison (see L instead.) =head2 required_modules This method returns a list of all the modules for which requirements have been specified. =head2 clone $req->clone; This method returns a clone of the invocant. The clone and the original object can then be changed independent of one another. =head2 is_simple This method returns true if and only if all requirements are inclusive minimums -- that is, if their string expression is just the version number. =head2 is_finalized This method returns true if the requirements have been finalized by having the C method called on them. =head2 finalize This method marks the requirements finalized. Subsequent attempts to change the requirements will be fatal, I they would result in a change. If they would not alter the requirements, they have no effect. If a finalized set of requirements is cloned, the cloned requirements are not also finalized. =head2 as_string_hash This returns a reference to a hash describing the requirements using the strings in the F specification. For example after the following program: my $req = CPAN::Meta::Requirements->new; $req->add_minimum('CPAN::Meta::Requirements' => 0.102); $req->add_minimum('Library::Foo' => 1.208); $req->add_maximum('Library::Foo' => 2.602); $req->add_minimum('Module::Bar' => 'v1.2.3'); $req->add_exclusion('Module::Bar' => 'v1.2.8'); $req->exact_version('Xyzzy' => '6.01'); my $hashref = $req->as_string_hash; C<$hashref> would contain: { 'CPAN::Meta::Requirements' => '0.102', 'Library::Foo' => '>= 1.208, <= 2.206', 'Module::Bar' => '>= v1.2.3, != v1.2.8', 'Xyzzy' => '== 6.01', } =head2 add_string_requirement $req->add_string_requirement('Library::Foo' => '>= 1.208, <= 2.206'); This method parses the passed in string and adds the appropriate requirement for the given module. It understands version ranges as described in the L. For example: =over 4 =item 1.3 =item >= 1.3 =item <= 1.3 =item == 1.3 =item != 1.3 =item > 1.3 =item < 1.3 =item >= 1.3, != 1.5, <= 2.0 A version number without an operator is equivalent to specifying a minimum (C=>). Extra whitespace is allowed. =back =head2 from_string_hash my $req = CPAN::Meta::Requirements->from_string_hash( \%hash ); This is an alternate constructor for a CPAN::Meta::Requirements object. It takes a hash of module names and version requirement strings and returns a new CPAN::Meta::Requirements object. =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/cpan-meta-requirements.git =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut PKnjZ: Converter.pmnu[use 5.006; use strict; use warnings; package CPAN::Meta::Converter; our $VERSION = '2.120921'; # VERSION use CPAN::Meta::Validator; use CPAN::Meta::Requirements; use version 0.88 (); use Parse::CPAN::Meta 1.4400 (); sub _dclone { my $ref = shift; # if an object is in the data structure and doesn't specify how to # turn itself into JSON, we just stringify the object. That does the # right thing for typical things that might be there, like version objects, # Path::Class objects, etc. no warnings 'once'; local *UNIVERSAL::TO_JSON = sub { return "$_[0]" }; my $backend = Parse::CPAN::Meta->json_backend(); return $backend->new->utf8->decode( $backend->new->utf8->allow_blessed->convert_blessed->encode($ref) ); } my %known_specs = ( '2' => 'http://search.cpan.org/perldoc?CPAN::Meta::Spec', '1.4' => 'http://module-build.sourceforge.net/META-spec-v1.4.html', '1.3' => 'http://module-build.sourceforge.net/META-spec-v1.3.html', '1.2' => 'http://module-build.sourceforge.net/META-spec-v1.2.html', '1.1' => 'http://module-build.sourceforge.net/META-spec-v1.1.html', '1.0' => 'http://module-build.sourceforge.net/META-spec-v1.0.html' ); my @spec_list = sort { $a <=> $b } keys %known_specs; my ($LOWEST, $HIGHEST) = @spec_list[0,-1]; #--------------------------------------------------------------------------# # converters # # called as $converter->($element, $field_name, $full_meta, $to_version) # # defined return value used for field # undef return value means field is skipped #--------------------------------------------------------------------------# sub _keep { $_[0] } sub _keep_or_one { defined($_[0]) ? $_[0] : 1 } sub _keep_or_zero { defined($_[0]) ? $_[0] : 0 } sub _keep_or_unknown { defined($_[0]) && length($_[0]) ? $_[0] : "unknown" } sub _generated_by { my $gen = shift; my $sig = __PACKAGE__ . " version " . (__PACKAGE__->VERSION || ""); return $sig unless defined $gen and length $gen; return $gen if $gen =~ /(, )\Q$sig/; return "$gen, $sig"; } sub _listify { ! defined $_[0] ? undef : ref $_[0] eq 'ARRAY' ? $_[0] : [$_[0]] } sub _prefix_custom { my $key = shift; $key =~ s/^(?!x_) # Unless it already starts with x_ (?:x-?)? # Remove leading x- or x (if present) /x_/ix; # and prepend x_ return $key; } sub _ucfirst_custom { my $key = shift; $key = ucfirst $key unless $key =~ /[A-Z]/; return $key; } sub _change_meta_spec { my ($element, undef, undef, $version) = @_; $element->{version} = $version; $element->{url} = $known_specs{$version}; return $element; } my @valid_licenses_1 = ( 'perl', 'gpl', 'apache', 'artistic', 'artistic_2', 'lgpl', 'bsd', 'gpl', 'mit', 'mozilla', 'open_source', 'unrestricted', 'restrictive', 'unknown', ); my %license_map_1 = ( ( map { $_ => $_ } @valid_licenses_1 ), artistic2 => 'artistic_2', ); sub _license_1 { my ($element) = @_; return 'unknown' unless defined $element; if ( $license_map_1{lc $element} ) { return $license_map_1{lc $element}; } return 'unknown'; } my @valid_licenses_2 = qw( agpl_3 apache_1_1 apache_2_0 artistic_1 artistic_2 bsd freebsd gfdl_1_2 gfdl_1_3 gpl_1 gpl_2 gpl_3 lgpl_2_1 lgpl_3_0 mit mozilla_1_0 mozilla_1_1 openssl perl_5 qpl_1_0 ssleay sun zlib open_source restricted unrestricted unknown ); # The "old" values were defined by Module::Build, and were often vague. I have # made the decisions below based on reading Module::Build::API and how clearly # it specifies the version of the license. my %license_map_2 = ( (map { $_ => $_ } @valid_licenses_2), apache => 'apache_2_0', # clearly stated as 2.0 artistic => 'artistic_1', # clearly stated as 1 artistic2 => 'artistic_2', # clearly stated as 2 gpl => 'open_source', # we don't know which GPL; punt lgpl => 'open_source', # we don't know which LGPL; punt mozilla => 'open_source', # we don't know which MPL; punt perl => 'perl_5', # clearly Perl 5 restrictive => 'restricted', ); sub _license_2 { my ($element) = @_; return [ 'unknown' ] unless defined $element; $element = [ $element ] unless ref $element eq 'ARRAY'; my @new_list; for my $lic ( @$element ) { next unless defined $lic; if ( my $new = $license_map_2{lc $lic} ) { push @new_list, $new; } } return @new_list ? \@new_list : [ 'unknown' ]; } my %license_downgrade_map = qw( agpl_3 open_source apache_1_1 apache apache_2_0 apache artistic_1 artistic artistic_2 artistic_2 bsd bsd freebsd open_source gfdl_1_2 open_source gfdl_1_3 open_source gpl_1 gpl gpl_2 gpl gpl_3 gpl lgpl_2_1 lgpl lgpl_3_0 lgpl mit mit mozilla_1_0 mozilla mozilla_1_1 mozilla openssl open_source perl_5 perl qpl_1_0 open_source ssleay open_source sun open_source zlib open_source open_source open_source restricted restrictive unrestricted unrestricted unknown unknown ); sub _downgrade_license { my ($element) = @_; if ( ! defined $element ) { return "unknown"; } elsif( ref $element eq 'ARRAY' ) { if ( @$element == 1 ) { return $license_downgrade_map{$element->[0]} || "unknown"; } } elsif ( ! ref $element ) { return $license_downgrade_map{$element} || "unknown"; } return "unknown"; } my $no_index_spec_1_2 = { 'file' => \&_listify, 'dir' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, }; my $no_index_spec_1_3 = { 'file' => \&_listify, 'directory' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, }; my $no_index_spec_2 = { 'file' => \&_listify, 'directory' => \&_listify, 'package' => \&_listify, 'namespace' => \&_listify, ':custom' => \&_prefix_custom, }; sub _no_index_1_2 { my (undef, undef, $meta) = @_; my $no_index = $meta->{no_index} || $meta->{private}; return unless $no_index; # cleanup wrong format if ( ! ref $no_index ) { my $item = $no_index; $no_index = { dir => [ $item ], file => [ $item ] }; } elsif ( ref $no_index eq 'ARRAY' ) { my $list = $no_index; $no_index = { dir => [ @$list ], file => [ @$list ] }; } # common mistake: files -> file if ( exists $no_index->{files} ) { $no_index->{file} = delete $no_index->{file}; } # common mistake: modules -> module if ( exists $no_index->{modules} ) { $no_index->{module} = delete $no_index->{module}; } return _convert($no_index, $no_index_spec_1_2); } sub _no_index_directory { my ($element, $key, $meta, $version) = @_; return unless $element; # cleanup wrong format if ( ! ref $element ) { my $item = $element; $element = { directory => [ $item ], file => [ $item ] }; } elsif ( ref $element eq 'ARRAY' ) { my $list = $element; $element = { directory => [ @$list ], file => [ @$list ] }; } if ( exists $element->{dir} ) { $element->{directory} = delete $element->{dir}; } # common mistake: files -> file if ( exists $element->{files} ) { $element->{file} = delete $element->{file}; } # common mistake: modules -> module if ( exists $element->{modules} ) { $element->{module} = delete $element->{module}; } my $spec = $version == 2 ? $no_index_spec_2 : $no_index_spec_1_3; return _convert($element, $spec); } sub _is_module_name { my $mod = shift; return unless defined $mod && length $mod; return $mod =~ m{^[A-Za-z][A-Za-z0-9_]*(?:::[A-Za-z0-9_]+)*$}; } sub _clean_version { my ($element, $key, $meta, $to_version) = @_; return 0 if ! defined $element; $element =~ s{^\s*}{}; $element =~ s{\s*$}{}; $element =~ s{^\.}{0.}; return 0 if ! length $element; return 0 if ( $element eq 'undef' || $element eq '' ); my $v = eval { version->new($element) }; # XXX check defined $v and not just $v because version objects leak memory # in boolean context -- dagolden, 2012-02-03 if ( defined $v ) { return $v->is_qv ? $v->normal : $element; } else { return 0; } } sub _bad_version_hook { my ($v) = @_; $v =~ s{[a-z]+$}{}; # strip trailing alphabetics my $vobj = eval { version->parse($v) }; return defined($vobj) ? $vobj : version->parse(0); # or give up } sub _version_map { my ($element) = @_; return unless defined $element; if ( ref $element eq 'HASH' ) { # XXX turn this into CPAN::Meta::Requirements with bad version hook # and then turn it back into a hash my $new_map = CPAN::Meta::Requirements->new( { bad_version_hook => sub { version->new(0) } } # punt ); while ( my ($k,$v) = each %$element ) { next unless _is_module_name($k); if ( !defined($v) || !length($v) || $v eq 'undef' || $v eq '' ) { $v = 0; } # some weird, old META have bad yml with module => module # so check if value is like a module name and not like a version if ( _is_module_name($v) && ! version::is_lax($v) ) { $new_map->add_minimum($k => 0); $new_map->add_minimum($v => 0); } $new_map->add_string_requirement($k => $v); } return $new_map->as_string_hash; } elsif ( ref $element eq 'ARRAY' ) { my $hashref = { map { $_ => 0 } @$element }; return _version_map($hashref); # cleanup any weird stuff } elsif ( ref $element eq '' && length $element ) { return { $element => 0 } } return; } sub _prereqs_from_1 { my (undef, undef, $meta) = @_; my $prereqs = {}; for my $phase ( qw/build configure/ ) { my $key = "${phase}_requires"; $prereqs->{$phase}{requires} = _version_map($meta->{$key}) if $meta->{$key}; } for my $rel ( qw/requires recommends conflicts/ ) { $prereqs->{runtime}{$rel} = _version_map($meta->{$rel}) if $meta->{$rel}; } return $prereqs; } my $prereqs_spec = { configure => \&_prereqs_rel, build => \&_prereqs_rel, test => \&_prereqs_rel, runtime => \&_prereqs_rel, develop => \&_prereqs_rel, ':custom' => \&_prefix_custom, }; my $relation_spec = { requires => \&_version_map, recommends => \&_version_map, suggests => \&_version_map, conflicts => \&_version_map, ':custom' => \&_prefix_custom, }; sub _cleanup_prereqs { my ($prereqs, $key, $meta, $to_version) = @_; return unless $prereqs && ref $prereqs eq 'HASH'; return _convert( $prereqs, $prereqs_spec, $to_version ); } sub _prereqs_rel { my ($relation, $key, $meta, $to_version) = @_; return unless $relation && ref $relation eq 'HASH'; return _convert( $relation, $relation_spec, $to_version ); } BEGIN { my @old_prereqs = qw( requires configure_requires recommends conflicts ); for ( @old_prereqs ) { my $sub = "_get_$_"; my ($phase,$type) = split qr/_/, $_; if ( ! defined $type ) { $type = $phase; $phase = 'runtime'; } no strict 'refs'; *{$sub} = sub { _extract_prereqs($_[2]->{prereqs},$phase,$type) }; } } sub _get_build_requires { my ($data, $key, $meta) = @_; my $test_h = _extract_prereqs($_[2]->{prereqs}, qw(test requires)) || {}; my $build_h = _extract_prereqs($_[2]->{prereqs}, qw(build requires)) || {}; my $test_req = CPAN::Meta::Requirements->from_string_hash($test_h); my $build_req = CPAN::Meta::Requirements->from_string_hash($build_h); $test_req->add_requirements($build_req)->as_string_hash; } sub _extract_prereqs { my ($prereqs, $phase, $type) = @_; return unless ref $prereqs eq 'HASH'; return scalar _version_map($prereqs->{$phase}{$type}); } sub _downgrade_optional_features { my (undef, undef, $meta) = @_; return unless exists $meta->{optional_features}; my $origin = $meta->{optional_features}; my $features = {}; for my $name ( keys %$origin ) { $features->{$name} = { description => $origin->{$name}{description}, requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','requires'), configure_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','configure_requires'), build_requires => _extract_prereqs($origin->{$name}{prereqs},'runtime','build_requires'), recommends => _extract_prereqs($origin->{$name}{prereqs},'runtime','recommends'), conflicts => _extract_prereqs($origin->{$name}{prereqs},'runtime','conflicts'), }; for my $k (keys %{$features->{$name}} ) { delete $features->{$name}{$k} unless defined $features->{$name}{$k}; } } return $features; } sub _upgrade_optional_features { my (undef, undef, $meta) = @_; return unless exists $meta->{optional_features}; my $origin = $meta->{optional_features}; my $features = {}; for my $name ( keys %$origin ) { $features->{$name} = { description => $origin->{$name}{description}, prereqs => _prereqs_from_1(undef, undef, $origin->{$name}), }; delete $features->{$name}{prereqs}{configure}; } return $features; } my $optional_features_2_spec = { description => \&_keep, prereqs => \&_cleanup_prereqs, ':custom' => \&_prefix_custom, }; sub _feature_2 { my ($element, $key, $meta, $to_version) = @_; return unless $element && ref $element eq 'HASH'; _convert( $element, $optional_features_2_spec, $to_version ); } sub _cleanup_optional_features_2 { my ($element, $key, $meta, $to_version) = @_; return unless $element && ref $element eq 'HASH'; my $new_data = {}; for my $k ( keys %$element ) { $new_data->{$k} = _feature_2( $element->{$k}, $k, $meta, $to_version ); } return unless keys %$new_data; return $new_data; } sub _optional_features_1_4 { my ($element) = @_; return unless $element; $element = _optional_features_as_map($element); for my $name ( keys %$element ) { for my $drop ( qw/requires_packages requires_os excluded_os/ ) { delete $element->{$name}{$drop}; } } return $element; } sub _optional_features_as_map { my ($element) = @_; return unless $element; if ( ref $element eq 'ARRAY' ) { my %map; for my $feature ( @$element ) { my (@parts) = %$feature; $map{$parts[0]} = $parts[1]; } $element = \%map; } return $element; } sub _is_urlish { defined $_[0] && $_[0] =~ m{\A[-+.a-z0-9]+:.+}i } sub _url_or_drop { my ($element) = @_; return $element if _is_urlish($element); return; } sub _url_list { my ($element) = @_; return unless $element; $element = _listify( $element ); $element = [ grep { _is_urlish($_) } @$element ]; return unless @$element; return $element; } sub _author_list { my ($element) = @_; return [ 'unknown' ] unless $element; $element = _listify( $element ); $element = [ map { defined $_ && length $_ ? $_ : 'unknown' } @$element ]; return [ 'unknown' ] unless @$element; return $element; } my $resource2_upgrade = { license => sub { return _is_urlish($_[0]) ? _listify( $_[0] ) : undef }, homepage => \&_url_or_drop, bugtracker => sub { my ($item) = @_; return unless $item; if ( $item =~ m{^mailto:(.*)$} ) { return { mailto => $1 } } elsif( _is_urlish($item) ) { return { web => $item } } else { return } }, repository => sub { return _is_urlish($_[0]) ? { url => $_[0] } : undef }, ':custom' => \&_prefix_custom, }; sub _upgrade_resources_2 { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource2_upgrade); } my $bugtracker2_spec = { web => \&_url_or_drop, mailto => \&_keep, ':custom' => \&_prefix_custom, }; sub _repo_type { my ($element, $key, $meta, $to_version) = @_; return $element if defined $element; return unless exists $meta->{url}; my $repo_url = $meta->{url}; for my $type ( qw/git svn/ ) { return $type if $repo_url =~ m{\A$type}; } return; } my $repository2_spec = { web => \&_url_or_drop, url => \&_url_or_drop, type => \&_repo_type, ':custom' => \&_prefix_custom, }; my $resources2_cleanup = { license => \&_url_list, homepage => \&_url_or_drop, bugtracker => sub { ref $_[0] ? _convert( $_[0], $bugtracker2_spec ) : undef }, repository => sub { my $data = shift; ref $data ? _convert( $data, $repository2_spec ) : undef }, ':custom' => \&_prefix_custom, }; sub _cleanup_resources_2 { my ($resources, $key, $meta, $to_version) = @_; return unless $resources && ref $resources eq 'HASH'; return _convert($resources, $resources2_cleanup, $to_version); } my $resource1_spec = { license => \&_url_or_drop, homepage => \&_url_or_drop, bugtracker => \&_url_or_drop, repository => \&_url_or_drop, ':custom' => \&_keep, }; sub _resources_1_3 { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource1_spec); } *_resources_1_4 = *_resources_1_3; sub _resources_1_2 { my (undef, undef, $meta) = @_; my $resources = $meta->{resources} || {}; if ( $meta->{license_url} && ! $resources->{license} ) { $resources->{license} = $meta->license_url if _is_urlish($meta->{license_url}); } return unless keys %$resources; return _convert($resources, $resource1_spec); } my $resource_downgrade_spec = { license => sub { return ref $_[0] ? $_[0]->[0] : $_[0] }, homepage => \&_url_or_drop, bugtracker => sub { return $_[0]->{web} }, repository => sub { return $_[0]->{url} || $_[0]->{web} }, ':custom' => \&_ucfirst_custom, }; sub _downgrade_resources { my (undef, undef, $meta, $version) = @_; return unless exists $meta->{resources}; return _convert($meta->{resources}, $resource_downgrade_spec); } sub _release_status { my ($element, undef, $meta) = @_; return $element if $element && $element =~ m{\A(?:stable|testing|unstable)\z}; return _release_status_from_version(undef, undef, $meta); } sub _release_status_from_version { my (undef, undef, $meta) = @_; my $version = $meta->{version} || ''; return ( $version =~ /_/ ) ? 'testing' : 'stable'; } my $provides_spec = { file => \&_keep, version => \&_clean_version, }; my $provides_spec_2 = { file => \&_keep, version => \&_clean_version, ':custom' => \&_prefix_custom, }; sub _provides { my ($element, $key, $meta, $to_version) = @_; return unless defined $element && ref $element eq 'HASH'; my $spec = $to_version == 2 ? $provides_spec_2 : $provides_spec; my $new_data = {}; for my $k ( keys %$element ) { $new_data->{$k} = _convert($element->{$k}, $spec, $to_version); } return $new_data; } sub _convert { my ($data, $spec, $to_version) = @_; my $new_data = {}; for my $key ( keys %$spec ) { next if $key eq ':custom' || $key eq ':drop'; next unless my $fcn = $spec->{$key}; die "spec for '$key' is not a coderef" unless ref $fcn && ref $fcn eq 'CODE'; my $new_value = $fcn->($data->{$key}, $key, $data, $to_version); $new_data->{$key} = $new_value if defined $new_value; } my $drop_list = $spec->{':drop'}; my $customizer = $spec->{':custom'} || \&_keep; for my $key ( keys %$data ) { next if $drop_list && grep { $key eq $_ } @$drop_list; next if exists $spec->{$key}; # we handled it $new_data->{ $customizer->($key) } = $data->{$key}; } return $new_data; } #--------------------------------------------------------------------------# # define converters for each conversion #--------------------------------------------------------------------------# # each converts from prior version # special ":custom" field is used for keys not recognized in spec my %up_convert = ( '2-from-1.4' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_2, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # CHANGED TO MANDATORY 'dynamic_config' => \&_keep_or_one, # ADDED MANDATORY 'release_status' => \&_release_status_from_version, # PRIOR OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_upgrade_optional_features, 'provides' => \&_provides, 'resources' => \&_upgrade_resources_2, # ADDED OPTIONAL 'description' => \&_keep, 'prereqs' => \&_prereqs_from_1, # drop these deprecated fields, but only after we convert ':drop' => [ qw( build_requires configure_requires conflicts distribution_type license_url private recommends requires ) ], # other random keys need x_ prefixing ':custom' => \&_prefix_custom, }, '1.4-from-1.3' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_1_4, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_4, # ADDED OPTIONAL 'configure_requires' => \&_keep, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.3-from-1.2' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.2-from-1.1' => { # PRIOR MANDATORY 'version' => \&_keep, # CHANGED TO MANDATORY 'license' => \&_license_1, 'name' => \&_keep, 'generated_by' => \&_generated_by, # ADDED MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'resources' => \&_resources_1_2, # drop these deprecated fields, but only after we convert ':drop' => [ qw( license_url private )], # other random keys are OK if already valid ':custom' => \&_keep }, '1.1-from-1.0' => { # CHANGED TO MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'license_url' => \&_url_or_drop, 'private' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, ); my %down_convert = ( '1.4-from-2' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_downgrade_license, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_get_build_requires, 'configure_requires' => \&_get_configure_requires, 'conflicts' => \&_get_conflicts, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_downgrade_optional_features, 'provides' => \&_provides, 'recommends' => \&_get_recommends, 'requires' => \&_get_requires, 'resources' => \&_downgrade_resources, # drop these unsupported fields (after conversion) ':drop' => [ qw( description prereqs release_status )], # custom keys will be left unchanged ':custom' => \&_keep }, '1.3-from-1.4' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # drop these unsupported fields, but only after we convert ':drop' => [ qw( configure_requires )], # other random keys are OK if already valid ':custom' => \&_keep, }, '1.2-from-1.3' => { # MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # other random keys are OK if already valid ':custom' => \&_keep, }, '1.1-from-1.2' => { # MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, # OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'private' => \&_keep, 'recommends' => \&_version_map, 'requires' => \&_version_map, # drop unsupported fields ':drop' => [ qw( abstract author provides no_index keywords resources )], # other random keys are OK if already valid ':custom' => \&_keep, }, '1.0-from-1.1' => { # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # other random keys are OK if already valid ':custom' => \&_keep, }, ); my %cleanup = ( '2' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_2, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # CHANGED TO MANDATORY 'dynamic_config' => \&_keep_or_one, # ADDED MANDATORY 'release_status' => \&_release_status, # PRIOR OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_cleanup_optional_features_2, 'provides' => \&_provides, 'resources' => \&_cleanup_resources_2, # ADDED OPTIONAL 'description' => \&_keep, 'prereqs' => \&_cleanup_prereqs, # drop these deprecated fields, but only after we convert ':drop' => [ qw( build_requires configure_requires conflicts distribution_type license_url private recommends requires ) ], # other random keys need x_ prefixing ':custom' => \&_prefix_custom, }, '1.4' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_1_4, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_4, # ADDED OPTIONAL 'configure_requires' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, '1.3' => { # PRIOR MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'meta-spec' => \&_change_meta_spec, 'name' => \&_keep, 'version' => \&_keep, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'keywords' => \&_keep, 'no_index' => \&_no_index_directory, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'recommends' => \&_version_map, 'requires' => \&_version_map, 'resources' => \&_resources_1_3, # other random keys are OK if already valid ':custom' => \&_keep }, '1.2' => { # PRIOR MANDATORY 'version' => \&_keep, # CHANGED TO MANDATORY 'license' => \&_license_1, 'name' => \&_keep, 'generated_by' => \&_generated_by, # ADDED MANDATORY 'abstract' => \&_keep_or_unknown, 'author' => \&_author_list, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'keywords' => \&_keep, 'no_index' => \&_no_index_1_2, 'optional_features' => \&_optional_features_as_map, 'provides' => \&_provides, 'resources' => \&_resources_1_2, # other random keys are OK if already valid ':custom' => \&_keep }, '1.1' => { # CHANGED TO MANDATORY 'version' => \&_keep, # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, # PRIOR OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # ADDED OPTIONAL 'license_url' => \&_url_or_drop, 'private' => \&_keep, # other random keys are OK if already valid ':custom' => \&_keep }, '1.0' => { # IMPLIED MANDATORY 'name' => \&_keep, 'meta-spec' => \&_change_meta_spec, 'version' => \&_keep, # IMPLIED OPTIONAL 'build_requires' => \&_version_map, 'conflicts' => \&_version_map, 'distribution_type' => \&_keep, 'dynamic_config' => \&_keep_or_one, 'generated_by' => \&_generated_by, 'license' => \&_license_1, 'recommends' => \&_version_map, 'requires' => \&_version_map, # other random keys are OK if already valid ':custom' => \&_keep, }, ); #--------------------------------------------------------------------------# # Code #--------------------------------------------------------------------------# sub new { my ($class,$data) = @_; # create an attributes hash my $self = { 'data' => $data, 'spec' => $data->{'meta-spec'}{'version'} || "1.0", }; # create the object return bless $self, $class; } sub convert { my ($self, %args) = @_; my $args = { %args }; my $new_version = $args->{version} || $HIGHEST; my ($old_version) = $self->{spec}; my $converted = _dclone($self->{data}); if ( $old_version == $new_version ) { $converted = _convert( $converted, $cleanup{$old_version}, $old_version ); my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to clean-up $old_version metadata. Errors:\n$errs\n"; } return $converted; } elsif ( $old_version > $new_version ) { my @vers = sort { $b <=> $a } keys %known_specs; for my $i ( 0 .. $#vers-1 ) { next if $vers[$i] > $old_version; last if $vers[$i+1] < $new_version; my $spec_string = "$vers[$i+1]-from-$vers[$i]"; $converted = _convert( $converted, $down_convert{$spec_string}, $vers[$i+1] ); my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to downconvert metadata to $vers[$i+1]. Errors:\n$errs\n"; } } return $converted; } else { my @vers = sort { $a <=> $b } keys %known_specs; for my $i ( 0 .. $#vers-1 ) { next if $vers[$i] < $old_version; last if $vers[$i+1] > $new_version; my $spec_string = "$vers[$i+1]-from-$vers[$i]"; $converted = _convert( $converted, $up_convert{$spec_string}, $vers[$i+1] ); my $cmv = CPAN::Meta::Validator->new( $converted ); unless ( $cmv->is_valid ) { my $errs = join("\n", $cmv->errors); die "Failed to upconvert metadata to $vers[$i+1]. Errors:\n$errs\n"; } } return $converted; } } 1; # ABSTRACT: Convert CPAN distribution metadata structures =pod =head1 NAME CPAN::Meta::Converter - Convert CPAN distribution metadata structures =head1 VERSION version 2.120921 =head1 SYNOPSIS my $struct = decode_json_file('META.json'); my $cmc = CPAN::Meta::Converter->new( $struct ); my $new_struct = $cmc->convert( version => "2" ); =head1 DESCRIPTION This module converts CPAN Meta structures from one form to another. The primary use is to convert older structures to the most modern version of the specification, but other transformations may be implemented in the future as needed. (E.g. stripping all custom fields or stripping all optional fields.) =head1 METHODS =head2 new my $cmc = CPAN::Meta::Converter->new( $struct ); The constructor should be passed a valid metadata structure but invalid structures are accepted. If no meta-spec version is provided, version 1.0 will be assumed. =head2 convert my $new_struct = $cmc->convert( version => "2" ); Returns a new hash reference with the metadata converted to a different form. C will die if any conversion/standardization still results in an invalid structure. Valid parameters include: =over =item * C -- Indicates the desired specification version (e.g. "1.0", "1.1" ... "1.4", "2"). Defaults to the latest version of the CPAN Meta Spec. =back Conversion proceeds through each version in turn. For example, a version 1.2 structure might be converted to 1.3 then 1.4 then finally to version 2. The conversion process attempts to clean-up simple errors and standardize data. For example, if C is given as a scalar, it will converted to an array reference containing the item. (Converting a structure to its own version will also clean-up and standardize.) When data are cleaned and standardized, missing or invalid fields will be replaced with sensible defaults when possible. This may be lossy or imprecise. For example, some badly structured META.yml files on CPAN have prerequisite modules listed as both keys and values: requires => { 'Foo::Bar' => 'Bam::Baz' } These would be split and each converted to a prerequisite with a minimum version of zero. When some mandatory fields are missing or invalid, the conversion will attempt to provide a sensible default or will fill them with a value of 'unknown'. For example a missing or unrecognized C field will result in a C field of 'unknown'. Fields that may get an 'unknown' include: =over 4 =item * abstract =item * author =item * license =back =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ PKnjZ^kY Feature.pmnu[use 5.006; use strict; use warnings; package CPAN::Meta::Feature; our $VERSION = '2.120921'; # VERSION use CPAN::Meta::Prereqs; sub new { my ($class, $identifier, $spec) = @_; my %guts = ( identifier => $identifier, description => $spec->{description}, prereqs => CPAN::Meta::Prereqs->new($spec->{prereqs}), ); bless \%guts => $class; } sub identifier { $_[0]{identifier} } sub description { $_[0]{description} } sub prereqs { $_[0]{prereqs} } 1; # ABSTRACT: an optional feature provided by a CPAN distribution =pod =head1 NAME CPAN::Meta::Feature - an optional feature provided by a CPAN distribution =head1 VERSION version 2.120921 =head1 DESCRIPTION A CPAN::Meta::Feature object describes an optional feature offered by a CPAN distribution and specified in the distribution's F (or F) file. For the most part, this class will only be used when operating on the result of the C or C methods on a L object. =head1 METHODS =head2 new my $feature = CPAN::Meta::Feature->new( $identifier => \%spec ); This returns a new Feature object. The C<%spec> argument to the constructor should be the same as the value of the C entry in the distmeta. It must contain entries for C and C. =head2 identifier This method returns the feature's identifier. =head2 description This method returns the feature's long description. =head2 prereqs This method returns the feature's prerequisites as a L object. =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ PKnjZXDDYAML.pmnu[package CPAN::Meta::YAML; { $CPAN::Meta::YAML::VERSION = '0.008'; } use strict; # UTF Support? sub HAVE_UTF8 () { $] >= 5.007003 } BEGIN { if ( HAVE_UTF8 ) { # The string eval helps hide this from Test::MinimumVersion eval "require utf8;"; die "Failed to load UTF-8 support" if $@; } # Class structure require 5.004; require Exporter; require Carp; @CPAN::Meta::YAML::ISA = qw{ Exporter }; @CPAN::Meta::YAML::EXPORT = qw{ Load Dump }; @CPAN::Meta::YAML::EXPORT_OK = qw{ LoadFile DumpFile freeze thaw }; # Error storage $CPAN::Meta::YAML::errstr = ''; } # The character class of all characters we need to escape # NOTE: Inlined, since it's only used once # my $RE_ESCAPE = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f\"\n]'; # Printed form of the unprintable characters in the lowest range # of ASCII characters, listed by ASCII ordinal position. my @UNPRINTABLE = qw( z x01 x02 x03 x04 x05 x06 a x08 t n v f r x0e x0f x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x1a e x1c x1d x1e x1f ); # Printable characters for escapes my %UNESCAPES = ( z => "\x00", a => "\x07", t => "\x09", n => "\x0a", v => "\x0b", f => "\x0c", r => "\x0d", e => "\x1b", '\\' => '\\', ); # Special magic boolean words my %QUOTE = map { $_ => 1 } qw{ null Null NULL y Y yes Yes YES n N no No NO true True TRUE false False FALSE on On ON off Off OFF }; ##################################################################### # Implementation # Create an empty CPAN::Meta::YAML object sub new { my $class = shift; bless [ @_ ], $class; } # Create an object from a file sub read { my $class = ref $_[0] ? ref shift : shift; # Check the file my $file = shift or return $class->_error( 'You did not specify a file name' ); return $class->_error( "File '$file' does not exist" ) unless -e $file; return $class->_error( "'$file' is a directory, not a file" ) unless -f _; return $class->_error( "Insufficient permissions to read '$file'" ) unless -r _; # Slurp in the file local $/ = undef; local *CFG; unless ( open(CFG, $file) ) { return $class->_error("Failed to open file '$file': $!"); } my $contents = ; unless ( close(CFG) ) { return $class->_error("Failed to close file '$file': $!"); } $class->read_string( $contents ); } # Create an object from a string sub read_string { my $class = ref $_[0] ? ref shift : shift; my $self = bless [], $class; my $string = $_[0]; eval { unless ( defined $string ) { die \"Did not provide a string to load"; } # Byte order marks # NOTE: Keeping this here to educate maintainers # my %BOM = ( # "\357\273\277" => 'UTF-8', # "\376\377" => 'UTF-16BE', # "\377\376" => 'UTF-16LE', # "\377\376\0\0" => 'UTF-32LE' # "\0\0\376\377" => 'UTF-32BE', # ); if ( $string =~ /^(?:\376\377|\377\376|\377\376\0\0|\0\0\376\377)/ ) { die \"Stream has a non UTF-8 BOM"; } else { # Strip UTF-8 bom if found, we'll just ignore it $string =~ s/^\357\273\277//; } # Try to decode as utf8 utf8::decode($string) if HAVE_UTF8; # Check for some special cases return $self unless length $string; unless ( $string =~ /[\012\015]+\z/ ) { die \"Stream does not end with newline character"; } # Split the file into lines my @lines = grep { ! /^\s*(?:\#.*)?\z/ } split /(?:\015{1,2}\012|\015|\012)/, $string; # Strip the initial YAML header @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines; # A nibbling parser while ( @lines ) { # Do we have a document header? if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) { # Handle scalar documents shift @lines; if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) { push @$self, $self->_read_scalar( "$1", [ undef ], \@lines ); next; } } if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) { # A naked document push @$self, undef; while ( @lines and $lines[0] !~ /^---/ ) { shift @lines; } } elsif ( $lines[0] =~ /^\s*\-/ ) { # An array at the root my $document = [ ]; push @$self, $document; $self->_read_array( $document, [ 0 ], \@lines ); } elsif ( $lines[0] =~ /^(\s*)\S/ ) { # A hash at the root my $document = { }; push @$self, $document; $self->_read_hash( $document, [ length($1) ], \@lines ); } else { die \"CPAN::Meta::YAML failed to classify the line '$lines[0]'"; } } }; if ( ref $@ eq 'SCALAR' ) { return $self->_error(${$@}); } elsif ( $@ ) { require Carp; Carp::croak($@); } return $self; } # Deparse a scalar string to the actual scalar sub _read_scalar { my ($self, $string, $indent, $lines) = @_; # Trim trailing whitespace $string =~ s/\s*\z//; # Explitic null/undef return undef if $string eq '~'; # Single quote if ( $string =~ /^\'(.*?)\'(?:\s+\#.*)?\z/ ) { return '' unless defined $1; $string = $1; $string =~ s/\'\'/\'/g; return $string; } # Double quote. # The commented out form is simpler, but overloaded the Perl regex # engine due to recursion and backtracking problems on strings # larger than 32,000ish characters. Keep it for reference purposes. # if ( $string =~ /^\"((?:\\.|[^\"])*)\"\z/ ) { if ( $string =~ /^\"([^\\"]*(?:\\.[^\\"]*)*)\"(?:\s+\#.*)?\z/ ) { # Reusing the variable is a little ugly, # but avoids a new variable and a string copy. $string = $1; $string =~ s/\\"/"/g; $string =~ s/\\([never\\fartz]|x([0-9a-fA-F]{2}))/(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}/gex; return $string; } # Special cases if ( $string =~ /^[\'\"!&]/ ) { die \"CPAN::Meta::YAML does not support a feature in line '$string'"; } return {} if $string =~ /^{}(?:\s+\#.*)?\z/; return [] if $string =~ /^\[\](?:\s+\#.*)?\z/; # Regular unquoted string if ( $string !~ /^[>|]/ ) { if ( $string =~ /^(?:-(?:\s|$)|[\@\%\`])/ or $string =~ /:(?:\s|$)/ ) { die \"CPAN::Meta::YAML found illegal characters in plain scalar: '$string'"; } $string =~ s/\s+#.*\z//; return $string; } # Error die \"CPAN::Meta::YAML failed to find multi-line scalar content" unless @$lines; # Check the indent depth $lines->[0] =~ /^(\s*)/; $indent->[-1] = length("$1"); if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } # Pull the lines my @multiline = (); while ( @$lines ) { $lines->[0] =~ /^(\s*)/; last unless length($1) >= $indent->[-1]; push @multiline, substr(shift(@$lines), length($1)); } my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n"; my $t = (substr($string, 1, 1) eq '-') ? '' : "\n"; return join( $j, @multiline ) . $t; } # Parse an array sub _read_array { my ($self, $array, $indent, $lines) = @_; while ( @$lines ) { # Check for a new document if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { while ( @$lines and $lines->[0] !~ /^---/ ) { shift @$lines; } return 1; } # Check the indent level $lines->[0] =~ /^(\s*)/; if ( length($1) < $indent->[-1] ) { return 1; } elsif ( length($1) > $indent->[-1] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) { # Inline nested hash my $indent2 = length("$1"); $lines->[0] =~ s/-/ /; push @$array, { }; $self->_read_hash( $array->[-1], [ @$indent, $indent2 ], $lines ); } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) { # Array entry with a value shift @$lines; push @$array, $self->_read_scalar( "$2", [ @$indent, undef ], $lines ); } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) { shift @$lines; unless ( @$lines ) { push @$array, undef; return 1; } if ( $lines->[0] =~ /^(\s*)\-/ ) { my $indent2 = length("$1"); if ( $indent->[-1] == $indent2 ) { # Null array entry push @$array, undef; } else { # Naked indenter push @$array, [ ]; $self->_read_array( $array->[-1], [ @$indent, $indent2 ], $lines ); } } elsif ( $lines->[0] =~ /^(\s*)\S/ ) { push @$array, { }; $self->_read_hash( $array->[-1], [ @$indent, length("$1") ], $lines ); } else { die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) { # This is probably a structure like the following... # --- # foo: # - list # bar: value # # ... so lets return and let the hash parser handle it return 1; } else { die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } } return 1; } # Parse an array sub _read_hash { my ($self, $hash, $indent, $lines) = @_; while ( @$lines ) { # Check for a new document if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) { while ( @$lines and $lines->[0] !~ /^---/ ) { shift @$lines; } return 1; } # Check the indent level $lines->[0] =~ /^(\s*)/; if ( length($1) < $indent->[-1] ) { return 1; } elsif ( length($1) > $indent->[-1] ) { die \"CPAN::Meta::YAML found bad indenting in line '$lines->[0]'"; } # Get the key unless ( $lines->[0] =~ s/^\s*([^\'\" ][^\n]*?)\s*:(\s+(?:\#.*)?|$)// ) { if ( $lines->[0] =~ /^\s*[?\'\"]/ ) { die \"CPAN::Meta::YAML does not support a feature in line '$lines->[0]'"; } die \"CPAN::Meta::YAML failed to classify line '$lines->[0]'"; } my $key = $1; # Do we have a value? if ( length $lines->[0] ) { # Yes $hash->{$key} = $self->_read_scalar( shift(@$lines), [ @$indent, undef ], $lines ); } else { # An indent shift @$lines; unless ( @$lines ) { $hash->{$key} = undef; return 1; } if ( $lines->[0] =~ /^(\s*)-/ ) { $hash->{$key} = []; $self->_read_array( $hash->{$key}, [ @$indent, length($1) ], $lines ); } elsif ( $lines->[0] =~ /^(\s*)./ ) { my $indent2 = length("$1"); if ( $indent->[-1] >= $indent2 ) { # Null hash entry $hash->{$key} = undef; } else { $hash->{$key} = {}; $self->_read_hash( $hash->{$key}, [ @$indent, length($1) ], $lines ); } } } } return 1; } # Save an object to a file sub write { my $self = shift; my $file = shift or return $self->_error('No file name provided'); # Write it to the file open( CFG, '>' . $file ) or return $self->_error( "Failed to open file '$file' for writing: $!" ); print CFG $self->write_string; close CFG; return 1; } # Save an object to a string sub write_string { my $self = shift; return '' unless @$self; # Iterate over the documents my $indent = 0; my @lines = (); foreach my $cursor ( @$self ) { push @lines, '---'; # An empty document if ( ! defined $cursor ) { # Do nothing # A scalar document } elsif ( ! ref $cursor ) { $lines[-1] .= ' ' . $self->_write_scalar( $cursor, $indent ); # A list at the root } elsif ( ref $cursor eq 'ARRAY' ) { unless ( @$cursor ) { $lines[-1] .= ' []'; next; } push @lines, $self->_write_array( $cursor, $indent, {} ); # A hash at the root } elsif ( ref $cursor eq 'HASH' ) { unless ( %$cursor ) { $lines[-1] .= ' {}'; next; } push @lines, $self->_write_hash( $cursor, $indent, {} ); } else { Carp::croak("Cannot serialize " . ref($cursor)); } } join '', map { "$_\n" } @lines; } sub _write_scalar { my $string = $_[1]; return '~' unless defined $string; return "''" unless length $string; if ( $string =~ /[\x00-\x08\x0b-\x0d\x0e-\x1f\"\'\n]/ ) { $string =~ s/\\/\\\\/g; $string =~ s/"/\\"/g; $string =~ s/\n/\\n/g; $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g; return qq|"$string"|; } if ( $string =~ /(?:^\W|\s|:\z)/ or $QUOTE{$string} ) { return "'$string'"; } return $string; } sub _write_array { my ($self, $array, $indent, $seen) = @_; if ( $seen->{refaddr($array)}++ ) { die "CPAN::Meta::YAML does not support circular references"; } my @lines = (); foreach my $el ( @$array ) { my $line = (' ' x $indent) . '-'; my $type = ref $el; if ( ! $type ) { $line .= ' ' . $self->_write_scalar( $el, $indent + 1 ); push @lines, $line; } elsif ( $type eq 'ARRAY' ) { if ( @$el ) { push @lines, $line; push @lines, $self->_write_array( $el, $indent + 1, $seen ); } else { $line .= ' []'; push @lines, $line; } } elsif ( $type eq 'HASH' ) { if ( keys %$el ) { push @lines, $line; push @lines, $self->_write_hash( $el, $indent + 1, $seen ); } else { $line .= ' {}'; push @lines, $line; } } else { die "CPAN::Meta::YAML does not support $type references"; } } @lines; } sub _write_hash { my ($self, $hash, $indent, $seen) = @_; if ( $seen->{refaddr($hash)}++ ) { die "CPAN::Meta::YAML does not support circular references"; } my @lines = (); foreach my $name ( sort keys %$hash ) { my $el = $hash->{$name}; my $line = (' ' x $indent) . "$name:"; my $type = ref $el; if ( ! $type ) { $line .= ' ' . $self->_write_scalar( $el, $indent + 1 ); push @lines, $line; } elsif ( $type eq 'ARRAY' ) { if ( @$el ) { push @lines, $line; push @lines, $self->_write_array( $el, $indent + 1, $seen ); } else { $line .= ' []'; push @lines, $line; } } elsif ( $type eq 'HASH' ) { if ( keys %$el ) { push @lines, $line; push @lines, $self->_write_hash( $el, $indent + 1, $seen ); } else { $line .= ' {}'; push @lines, $line; } } else { die "CPAN::Meta::YAML does not support $type references"; } } @lines; } # Set error sub _error { $CPAN::Meta::YAML::errstr = $_[1]; undef; } # Retrieve error sub errstr { $CPAN::Meta::YAML::errstr; } ##################################################################### # YAML Compatibility sub Dump { CPAN::Meta::YAML->new(@_)->write_string; } sub Load { my $self = CPAN::Meta::YAML->read_string(@_); unless ( $self ) { Carp::croak("Failed to load YAML document from string"); } if ( wantarray ) { return @$self; } else { # To match YAML.pm, return the last document return $self->[-1]; } } BEGIN { *freeze = *Dump; *thaw = *Load; } sub DumpFile { my $file = shift; CPAN::Meta::YAML->new(@_)->write($file); } sub LoadFile { my $self = CPAN::Meta::YAML->read($_[0]); unless ( $self ) { Carp::croak("Failed to load YAML document from '" . ($_[0] || '') . "'"); } if ( wantarray ) { return @$self; } else { # Return only the last document to match YAML.pm, return $self->[-1]; } } ##################################################################### # Use Scalar::Util if possible, otherwise emulate it BEGIN { local $@; eval { require Scalar::Util; }; my $v = eval("$Scalar::Util::VERSION") || 0; if ( $@ or $v < 1.18 ) { eval <<'END_PERL'; # Scalar::Util failed to load or too old sub refaddr { my $pkg = ref($_[0]) or return undef; if ( !! UNIVERSAL::can($_[0], 'can') ) { bless $_[0], 'Scalar::Util::Fake'; } else { $pkg = undef; } "$_[0]" =~ /0x(\w+)/; my $i = do { local $^W; hex $1 }; bless $_[0], $pkg if defined $pkg; $i; } END_PERL } else { *refaddr = *Scalar::Util::refaddr; } } 1; =pod =head1 NAME CPAN::Meta::YAML - Read and write a subset of YAML for CPAN Meta files =head1 VERSION version 0.008 =head1 SYNOPSIS use CPAN::Meta::YAML; # reading a META file open $fh, "<:utf8", "META.yml"; $yaml_text = do { local $/; <$fh> }; $yaml = CPAN::Meta::YAML->read_string($yaml_text) or die CPAN::Meta::YAML->errstr; # finding the metadata $meta = $yaml->[0]; # writing a META file $yaml_text = $yaml->write_string or die CPAN::Meta::YAML->errstr; open $fh, ">:utf8", "META.yml"; print $fh $yaml_text; =head1 DESCRIPTION This module implements a subset of the YAML specification for use in reading and writing CPAN metadata files like F and F. It should not be used for any other general YAML parsing or generation task. NOTE: F (and F) files should be UTF-8 encoded. Users are responsible for proper encoding and decoding. In particular, the C and C methods do B support UTF-8 and should not be used. =head1 SUPPORT This module is currently derived from L by Adam Kennedy. If there are bugs in how it parses a particular META.yml file, please file a bug report in the YAML::Tiny bugtracker: L =head1 SEE ALSO L, L, L =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan =head1 SUPPORT =head2 Bugs / Feature Requests Please report any bugs or feature requests through the issue tracker at L. You will be notified automatically of any progress on your issue. =head2 Source Code This is open source software. The code repository is available for public review and contribution under the terms of the license. L git clone https://github.com/dagolden/cpan-meta-yaml.git =head1 AUTHORS =over 4 =item * Adam Kennedy =item * David Golden =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by Adam Kennedy. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ # ABSTRACT: Read and write a subset of YAML for CPAN Meta files PKnjZXi History.pmnu[# vi:tw=72 use 5.006; use strict; use warnings; package CPAN::Meta::History; our $VERSION = '2.120921'; # VERSION 1; # ABSTRACT: history of CPAN Meta Spec changes __END__ =pod =head1 NAME CPAN::Meta::History - history of CPAN Meta Spec changes =head1 VERSION version 2.120921 =head1 DESCRIPTION The CPAN Meta Spec has gone through several iterations. It was originally written in HTML and later revised into POD (though published in HTML generated from the POD). Fields were added, removed or changed, sometimes by design and sometimes to reflect real-world usage after the fact. This document reconstructs the history of the CPAN Meta Spec based on change logs, repository commit messages and the published HTML files. In some cases, particularly prior to version 1.2, the exact version when certain fields were introduced or changed is inconsistent between sources. When in doubt, the published HTML files for versions 1.0 to 1.4 as they existed when version 2 was developed are used as the definitive source. Starting with version 2, the specification document is part of the CPAN-Meta distribution and will be published on CPAN as L. Going forward, specification version numbers will be integers and decimal portions will correspond to a release date for the CPAN::Meta library. =head1 HISTORY =head2 Version 2 April 2010 =over =item * Revised spec examples as perl data structures rather than YAML =item * Switched to JSON serialization from YAML =item * Specified allowed version number formats =item * Replaced 'requires', 'build_requires', 'configure_requires', 'recommends' and 'conflicts' with new 'prereqs' data structure divided by I (configure, build, test, runtime, etc.) and I (requires, recommends, suggests, conflicts) =item * Added support for 'develop' phase for requirements for maintaining a list of authoring tools =item * Changed 'license' to a list and revised the set of valid licenses =item * Made 'dynamic_config' mandatory to reduce confusion =item * Changed 'resources' subkey 'repository' to a hash that clarifies repository type, url for browsing and url for checkout =item * Changed 'resources' subkey 'bugtracker' to a hash for either web or mailto resource =item * Changed specification of 'optional_features': =over =item * Added formal specification and usage guide instead of just example =item * Changed to use new prereqs data structure instead of individual keys =back =item * Clarified intended use of 'author' as generalized contact list =item * Added 'release_status' field to indicate stable, testing or unstable status to provide hints to indexers =item * Added 'description' field for a longer description of the distribution =item * Formalized use of "x_" or "X_" for all custom keys not listed in the official spec =back =head2 Version 1.4 June 2008 =over =item * Noted explicit support for 'perl' in prerequisites =item * Added 'configure_requires' prerequisite type =item * Changed 'optional_features' =over =item * Example corrected to show map of maps instead of list of maps (though descriptive text said 'map' even in v1.3) =item * Removed 'requires_packages', 'requires_os' and 'excluded_os' as valid subkeys =back =back =head2 Version 1.3 November 2006 =over =item * Clarified that all prerequisites take version range specifications =item * Added 'no_index' subkey 'directory' and removed 'dir' to match actual usage in the wild =item * Added a 'repository' subkey to 'resources' =back =head2 Version 1.2 August 2005 =over =item * Re-wrote and restructured spec in POD syntax =item * Changed 'name' to be mandatory =item * Changed 'generated_by' to be mandatory =item * Changed 'license' to be mandatory =item * Added required 'abstract' field =item * Added required 'author' field =item * Added required 'meta-spec' field to define 'version' (and 'url') of the CPAN Meta Spec used for metadata =item * Added 'provides' field =item * Added 'no_index' field and deprecated 'private' field. 'no_index' subkeys include 'file', 'dir', 'package' and 'namespace' =item * Added 'keywords' field =item * Added 'resources' field with subkeys 'homepage', 'license', and 'bugtracker' =item * Added 'optional_features' field as an alternate under 'recommends'. Includes 'description', 'requires', 'build_requires', 'conflicts', 'requires_packages', 'requires_os' and 'excluded_os' as valid subkeys =item * Removed 'license_uri' field =back =head2 Version 1.1 May 2003 =over =item * Changed 'version' to be mandatory =item * Added 'private' field =item * Added 'license_uri' field =back =head2 Version 1.0 March 2003 =over =item * Original release (in HTML format only) =item * Included 'name', 'version', 'license', 'distribution_type', 'requires', 'recommends', 'build_requires', 'conflicts', 'dynamic_config', 'generated_by' =back =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut PKnjZ7#+:: Prereqs.pmnu[use 5.006; use strict; use warnings; package CPAN::Meta::Prereqs; our $VERSION = '2.120921'; # VERSION use Carp qw(confess); use Scalar::Util qw(blessed); use CPAN::Meta::Requirements 2.121; sub __legal_phases { qw(configure build test runtime develop) } sub __legal_types { qw(requires recommends suggests conflicts) } # expect a prereq spec from META.json -- rjbs, 2010-04-11 sub new { my ($class, $prereq_spec) = @_; $prereq_spec ||= {}; my %is_legal_phase = map {; $_ => 1 } $class->__legal_phases; my %is_legal_type = map {; $_ => 1 } $class->__legal_types; my %guts; PHASE: for my $phase (keys %$prereq_spec) { next PHASE unless $phase =~ /\Ax_/i or $is_legal_phase{$phase}; my $phase_spec = $prereq_spec->{ $phase }; next PHASE unless keys %$phase_spec; TYPE: for my $type (keys %$phase_spec) { next TYPE unless $type =~ /\Ax_/i or $is_legal_type{$type}; my $spec = $phase_spec->{ $type }; next TYPE unless keys %$spec; $guts{prereqs}{$phase}{$type} = CPAN::Meta::Requirements->from_string_hash( $spec ); } } return bless \%guts => $class; } sub requirements_for { my ($self, $phase, $type) = @_; confess "requirements_for called without phase" unless defined $phase; confess "requirements_for called without type" unless defined $type; unless ($phase =~ /\Ax_/i or grep { $phase eq $_ } $self->__legal_phases) { confess "requested requirements for unknown phase: $phase"; } unless ($type =~ /\Ax_/i or grep { $type eq $_ } $self->__legal_types) { confess "requested requirements for unknown type: $type"; } my $req = ($self->{prereqs}{$phase}{$type} ||= CPAN::Meta::Requirements->new); $req->finalize if $self->is_finalized; return $req; } sub with_merged_prereqs { my ($self, $other) = @_; my @other = blessed($other) ? $other : @$other; my @prereq_objs = ($self, @other); my %new_arg; for my $phase ($self->__legal_phases) { for my $type ($self->__legal_types) { my $req = CPAN::Meta::Requirements->new; for my $prereq (@prereq_objs) { my $this_req = $prereq->requirements_for($phase, $type); next unless $this_req->required_modules; $req->add_requirements($this_req); } next unless $req->required_modules; $new_arg{ $phase }{ $type } = $req->as_string_hash; } } return (ref $self)->new(\%new_arg); } sub as_string_hash { my ($self) = @_; my %hash; for my $phase ($self->__legal_phases) { for my $type ($self->__legal_types) { my $req = $self->requirements_for($phase, $type); next unless $req->required_modules; $hash{ $phase }{ $type } = $req->as_string_hash; } } return \%hash; } sub is_finalized { $_[0]{finalized} } sub finalize { my ($self) = @_; $self->{finalized} = 1; for my $phase (keys %{ $self->{prereqs} }) { $_->finalize for values %{ $self->{prereqs}{$phase} }; } } sub clone { my ($self) = @_; my $clone = (ref $self)->new( $self->as_string_hash ); } 1; # ABSTRACT: a set of distribution prerequisites by phase and type =pod =head1 NAME CPAN::Meta::Prereqs - a set of distribution prerequisites by phase and type =head1 VERSION version 2.120921 =head1 DESCRIPTION A CPAN::Meta::Prereqs object represents the prerequisites for a CPAN distribution or one of its optional features. Each set of prereqs is organized by phase and type, as described in L. =head1 METHODS =head2 new my $prereq = CPAN::Meta::Prereqs->new( \%prereq_spec ); This method returns a new set of Prereqs. The input should look like the contents of the C field described in L, meaning something more or less like this: my $prereq = CPAN::Meta::Prereqs->new({ runtime => { requires => { 'Some::Module' => '1.234', ..., }, ..., }, ..., }); You can also construct an empty set of prereqs with: my $prereqs = CPAN::Meta::Prereqs->new; This empty set of prereqs is useful for accumulating new prereqs before finally dumping the whole set into a structure or string. =head2 requirements_for my $requirements = $prereqs->requirements_for( $phase, $type ); This method returns a L object for the given phase/type combination. If no prerequisites are registered for that combination, a new CPAN::Meta::Requirements object will be returned, and it may be added to as needed. If C<$phase> or C<$type> are undefined or otherwise invalid, an exception will be raised. =head2 with_merged_prereqs my $new_prereqs = $prereqs->with_merged_prereqs( $other_prereqs ); my $new_prereqs = $prereqs->with_merged_prereqs( \@other_prereqs ); This method returns a new CPAN::Meta::Prereqs objects in which all the other prerequisites given are merged into the current set. This is primarily provided for combining a distribution's core prereqs with the prereqs of one of its optional features. The new prereqs object has no ties to the originals, and altering it further will not alter them. =head2 as_string_hash This method returns a hashref containing structures suitable for dumping into a distmeta data structure. It is made up of hashes and strings, only; there will be no Prereqs, CPAN::Meta::Requirements, or C objects inside it. =head2 is_finalized This method returns true if the set of prereqs has been marked "finalized," and cannot be altered. =head2 finalize Calling C on a Prereqs object will close it for further modification. Attempting to make any changes that would actually alter the prereqs will result in an exception being thrown. =head2 clone my $cloned_prereqs = $prereqs->clone; This method returns a Prereqs object that is identical to the original object, but can be altered without affecting the original object. Finalization does not survive cloning, meaning that you may clone a finalized set of prereqs and then modify the clone. =head1 BUGS Please report any bugs or feature using the CPAN Request Tracker. Bugs can be submitted through the web interface at L When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature. =head1 AUTHORS =over 4 =item * David Golden =item * Ricardo Signes =back =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2010 by David Golden and Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut __END__ PKnjZZvv Validator.pmnu[PKnjZ88wSpec.pmnu[PKnjZV2(2E2EqRequirements.pmnu[PKnjZ: BConverter.pmnu[PKnjZ^kY Feature.pmnu[PKnjZXDDYAML.pmnu[PKnjZXi 0History.pmnu[PKnjZ7#+:: EPrereqs.pmnu[PKS6`