summaryrefslogtreecommitdiff
path: root/lib/Module
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2014-08-29 04:48:18 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2014-08-29 04:48:18 +0000
commit6b1d736955543538c54f1d8033ce3bdcb175da91 (patch)
treef7f7e23929d493647c0e2f9b1a556d3101194538 /lib/Module
downloadModule-CPANfile-tarball-master.tar.gz
Module-CPANfile-1.1000HEADModule-CPANfile-1.1000master
Diffstat (limited to 'lib/Module')
-rw-r--r--lib/Module/CPANfile.pm323
-rw-r--r--lib/Module/CPANfile/Environment.pm173
-rw-r--r--lib/Module/CPANfile/Prereq.pm21
-rw-r--r--lib/Module/CPANfile/Prereqs.pm117
-rw-r--r--lib/Module/CPANfile/Requirement.pm25
5 files changed, 659 insertions, 0 deletions
diff --git a/lib/Module/CPANfile.pm b/lib/Module/CPANfile.pm
new file mode 100644
index 0000000..dc11a17
--- /dev/null
+++ b/lib/Module/CPANfile.pm
@@ -0,0 +1,323 @@
+package Module::CPANfile;
+use strict;
+use warnings;
+use Cwd;
+use Carp ();
+use Module::CPANfile::Environment;
+use Module::CPANfile::Requirement;
+
+our $VERSION = '1.1000';
+
+sub new {
+ my($class, $file) = @_;
+ bless {}, $class;
+}
+
+sub load {
+ my($proto, $file) = @_;
+
+ my $self = ref $proto ? $proto : $proto->new;
+ $self->parse($file || Cwd::abs_path('cpanfile'));
+ $self;
+}
+
+sub save {
+ my($self, $path) = @_;
+
+ open my $out, ">", $path or die "$path: $!";
+ print {$out} $self->to_string;
+}
+
+sub parse {
+ my($self, $file) = @_;
+
+ my $code = do {
+ open my $fh, "<", $file or die "$file: $!";
+ join '', <$fh>;
+ };
+
+ my $env = Module::CPANfile::Environment->new($file);
+ $env->parse($code) or die $@;
+
+ $self->{_mirrors} = $env->mirrors;
+ $self->{_prereqs} = $env->prereqs;
+}
+
+sub from_prereqs {
+ my($proto, $prereqs) = @_;
+
+ my $self = $proto->new;
+ $self->{_prereqs} = Module::CPANfile::Prereqs->from_cpan_meta($prereqs);
+
+ $self;
+}
+
+sub mirrors {
+ my $self = shift;
+ $self->{_mirrors} || [];
+}
+
+sub features {
+ my $self = shift;
+ map $self->feature($_), $self->{_prereqs}->identifiers;
+}
+
+sub feature {
+ my($self, $identifier) = @_;
+ $self->{_prereqs}->feature($identifier);
+}
+
+sub prereq { shift->prereqs }
+
+sub prereqs {
+ my $self = shift;
+ $self->{_prereqs}->as_cpan_meta;
+}
+
+sub merged_requirements {
+ my $self = shift;
+ $self->{_prereqs}->merged_requirements;
+}
+
+sub effective_prereqs {
+ my($self, $features) = @_;
+ $self->prereqs_with(@{$features || []});
+}
+
+sub prereqs_with {
+ my($self, @feature_identifiers) = @_;
+
+ my $prereqs = $self->prereqs;
+ my @others = map { $self->feature($_)->prereqs } @feature_identifiers;
+
+ $prereqs->with_merged_prereqs(\@others);
+}
+
+sub prereq_specs {
+ my $self = shift;
+ $self->prereqs->as_string_hash;
+}
+
+sub prereq_for_module {
+ my($self, $module) = @_;
+ $self->{_prereqs}->find($module);
+}
+
+sub options_for_module {
+ my($self, $module) = @_;
+ my $prereq = $self->prereq_for_module($module) or return;
+ $prereq->requirement->options;
+}
+
+sub merge_meta {
+ my($self, $file, $version) = @_;
+
+ require CPAN::Meta;
+
+ $version ||= $file =~ /\.yml$/ ? '1.4' : '2';
+
+ my $prereq = $self->prereqs;
+
+ my $meta = CPAN::Meta->load_file($file);
+ my $prereqs_hash = $prereq->with_merged_prereqs($meta->effective_prereqs)->as_string_hash;
+ my $struct = { %{$meta->as_struct}, prereqs => $prereqs_hash };
+
+ CPAN::Meta->new($struct)->save($file, { version => $version });
+}
+
+sub _dump {
+ my $str = shift;
+ require Data::Dumper;
+ chomp(my $value = Data::Dumper->new([$str])->Terse(1)->Dump);
+ $value;
+}
+
+sub to_string {
+ my($self, $include_empty) = @_;
+
+ my $mirrors = $self->mirrors;
+ my $prereqs = $self->prereq_specs;
+
+ my $code = '';
+ $code .= $self->_dump_mirrors($mirrors);
+ $code .= $self->_dump_prereqs($prereqs, $include_empty);
+
+ for my $feature ($self->features) {
+ $code .= sprintf "feature %s, %s => sub {\n", _dump($feature->{identifier}), _dump($feature->{description});
+ $code .= $self->_dump_prereqs($feature->{spec}, $include_empty, 4);
+ $code .= "}\n\n";
+ }
+
+ $code =~ s/\n+$/\n/s;
+ $code;
+}
+
+sub _dump_mirrors {
+ my($self, $mirrors) = @_;
+
+ my $code = "";
+
+ for my $url (@$mirrors) {
+ $code .= "mirror '$url';\n";
+ }
+
+ $code =~ s/\n+$/\n/s;
+ $code;
+}
+
+sub _dump_prereqs {
+ my($self, $prereqs, $include_empty, $base_indent) = @_;
+
+ my $code = '';
+ for my $phase (qw(runtime configure build test develop)) {
+ my $indent = $phase eq 'runtime' ? '' : ' ';
+ $indent = (' ' x ($base_indent || 0)) . $indent;
+
+ my($phase_code, $requirements);
+ $phase_code .= "on $phase => sub {\n" unless $phase eq 'runtime';
+
+ for my $type (qw(requires recommends suggests conflicts)) {
+ for my $mod (sort keys %{$prereqs->{$phase}{$type}}) {
+ my $ver = $prereqs->{$phase}{$type}{$mod};
+ $phase_code .= $ver eq '0'
+ ? "${indent}$type '$mod';\n"
+ : "${indent}$type '$mod', '$ver';\n";
+ $requirements++;
+ }
+ }
+
+ $phase_code .= "\n" unless $requirements;
+ $phase_code .= "};\n" unless $phase eq 'runtime';
+
+ $code .= $phase_code . "\n" if $requirements or $include_empty;
+ }
+
+ $code =~ s/\n+$/\n/s;
+ $code;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Module::CPANfile - Parse cpanfile
+
+=head1 SYNOPSIS
+
+ use Module::CPANfile;
+
+ my $file = Module::CPANfile->load("cpanfile");
+ my $prereqs = $file->prereqs; # CPAN::Meta::Prereqs object
+
+ my @features = $file->features; # CPAN::Meta::Feature objects
+ my $merged_prereqs = $file->prereqs_with(@identifiers); # CPAN::Meta::Prereqs
+
+ $file->merge_meta('MYMETA.json');
+
+=head1 DESCRIPTION
+
+Module::CPANfile is a tool to handle L<cpanfile> format to load application
+specific dependencies, not just for CPAN distributions.
+
+=head1 METHODS
+
+=over 4
+
+=item load
+
+ $file = Module::CPANfile->load;
+ $file = Module::CPANfile->load('cpanfile');
+
+Load and parse a cpanfile. By default it tries to load C<cpanfile> in
+the current directory, unless you pass the path to its argument.
+
+=item from_prereqs
+
+ $file = Module::CPANfile->from_prereqs({
+ runtime => { requires => { DBI => '1.000' } },
+ });
+
+Creates a new Module::CPANfile object from prereqs hash you can get
+via L<CPAN::Meta>'s C<prereqs>, or L<CPAN::Meta::Prereqs>'
+C<as_string_hash>.
+
+ # read MYMETA, then feed the prereqs to create Module::CPANfile
+ my $meta = CPAN::Meta->load_file('MYMETA.json');
+ my $file = Module::CPANfile->from_prereqs($meta->prereqs);
+
+ # load cpanfile, then recreate it with round-trip
+ my $file = Module::CPANfile->load('cpanfile');
+ $file = Module::CPANfile->from_prereqs($file->prereq_specs);
+ # or $file->prereqs->as_string_hash
+
+=item prereqs
+
+Returns L<CPAN::Meta::Prereqs> object out of the parsed cpanfile.
+
+=item prereq_specs
+
+Returns a hash reference that should be passed to C<< CPAN::Meta::Prereqs->new >>.
+
+=item features
+
+Returns a list of features available in the cpanfile as L<CPAN::Meta::Feature>.
+
+=item prereqs_with(@identifiers), effective_prereqs(\@identifiers)
+
+Returns L<CPAN::Meta::Prereqs> object, with merged prereqs for
+features identified with the C<@identifiers>.
+
+=item to_string($include_empty)
+
+ $file->to_string;
+ $file->to_string(1);
+
+Returns a canonical string (code) representation for cpanfile. Useful
+if you want to convert L<CPAN::Meta::Prereqs> to a new cpanfile.
+
+ # read MYMETA's prereqs and print cpanfile representation of it
+ my $meta = CPAN::Meta->load_file('MYMETA.json');
+ my $file = Module::CPANfile->from_prereqs($meta->prereqs);
+ print $file->to_string;
+
+By default, it omits the phase where there're no modules
+registered. If you pass the argument of a true value, it will print
+them as well.
+
+=item save
+
+ $file->save('cpanfile');
+
+Saves the currently loaded prereqs as a new C<cpanfile> by calling
+C<to_string>. Beware B<this method will overwrite the existing
+cpanfile without any warning or backup>. Taking a backup or giving
+warnings to users is a caller's responsibility.
+
+ # Read MYMETA.json and creates a new cpanfile
+ my $meta = CPAN::Meta->load_file('MYMETA.json');
+ my $file = Module::CPANfile->from_prereqs($meta->prereqs);
+ $file->save('cpanfile');
+
+=item merge_meta
+
+ $file->merge_meta('META.yml');
+ $file->merge_meta('MYMETA.json', '2.0');
+
+Merge the effective prereqs with Meta specification loaded from the
+given META file, using CPAN::Meta. You can specify the META spec
+version in the second argument, which defaults to 1.4 in case the
+given file is YAML, and 2 if it is JSON.
+
+=back
+
+=head1 AUTHOR
+
+Tatsuhiko Miyagawa
+
+=head1 SEE ALSO
+
+L<cpanfile>, L<CPAN::Meta>, L<CPAN::Meta::Spec>
+
+=cut
diff --git a/lib/Module/CPANfile/Environment.pm b/lib/Module/CPANfile/Environment.pm
new file mode 100644
index 0000000..e1c0ea1
--- /dev/null
+++ b/lib/Module/CPANfile/Environment.pm
@@ -0,0 +1,173 @@
+package Module::CPANfile::Environment;
+use strict;
+use warnings;
+use Module::CPANfile::Prereqs;
+use Carp ();
+
+my @bindings = qw(
+ on requires recommends suggests conflicts
+ feature
+ osname
+ mirror
+ configure_requires build_requires test_requires author_requires
+);
+
+my $file_id = 1;
+
+sub new {
+ my($class, $file) = @_;
+ bless {
+ file => $file,
+ phase => 'runtime', # default phase
+ feature => undef,
+ features => {},
+ prereqs => Module::CPANfile::Prereqs->new,
+ mirrors => [],
+ }, $class;
+}
+
+sub bind {
+ my $self = shift;
+ my $pkg = caller;
+
+ for my $binding (@bindings) {
+ no strict 'refs';
+ *{"$pkg\::$binding"} = sub { $self->$binding(@_) };
+ }
+}
+
+sub parse {
+ my($self, $code) = @_;
+
+ my $err;
+ {
+ local $@;
+ $file_id++;
+ $self->_evaluate(<<EVAL);
+package Module::CPANfile::Sandbox$file_id;
+no warnings;
+BEGIN { \$_environment->bind }
+
+# line 1 "$self->{file}"
+$code;
+EVAL
+ $err = $@;
+ }
+
+ if ($err) { die "Parsing $self->{file} failed: $err" };
+
+ return 1;
+}
+
+sub _evaluate {
+ my $_environment = $_[0];
+ eval $_[1];
+}
+
+sub prereqs { $_[0]->{prereqs} }
+
+sub mirrors { $_[0]->{mirrors} }
+
+# DSL goes from here
+
+sub on {
+ my($self, $phase, $code) = @_;
+ local $self->{phase} = $phase;
+ $code->();
+}
+
+sub feature {
+ my($self, $identifier, $description, $code) = @_;
+
+ # shortcut: feature identifier => sub { ... }
+ if (@_ == 3 && ref($description) eq 'CODE') {
+ $code = $description;
+ $description = $identifier;
+ }
+
+ unless (ref $description eq '' && ref $code eq 'CODE') {
+ Carp::croak("Usage: feature 'identifier', 'Description' => sub { ... }");
+ }
+
+ local $self->{feature} = $identifier;
+ $self->prereqs->add_feature($identifier, $description);
+
+ $code->();
+}
+
+sub osname { die "TODO" }
+
+sub mirror {
+ my($self, $url) = @_;
+ push @{$self->{mirrors}}, $url;
+}
+
+sub requirement_for {
+ my($self, $module, @args) = @_;
+
+ my $requirement = 0;
+ $requirement = shift @args if @args % 2;
+
+ return Module::CPANfile::Requirement->new(
+ name => $module,
+ version => $requirement,
+ @args,
+ );
+}
+
+sub requires {
+ my $self = shift;
+ $self->add_prereq(requires => @_);
+}
+
+sub recommends {
+ my $self = shift;
+ $self->add_prereq(recommends => @_);
+}
+
+sub suggests {
+ my $self = shift;
+ $self->add_prereq(suggests => @_);
+}
+
+sub conflicts {
+ my $self = shift;
+ $self->add_prereq(conflicts => @_);
+}
+
+sub add_prereq {
+ my($self, $type, $module, @args) = @_;
+
+ $self->prereqs->add_prereq(
+ feature => $self->{feature},
+ phase => $self->{phase},
+ type => $type,
+ module => $module,
+ requirement => $self->requirement_for($module, @args),
+ );
+}
+
+# Module::Install compatible shortcuts
+
+sub configure_requires {
+ my($self, @args) = @_;
+ $self->on(configure => sub { $self->requires(@args) });
+}
+
+sub build_requires {
+ my($self, @args) = @_;
+ $self->on(build => sub { $self->requires(@args) });
+}
+
+sub test_requires {
+ my($self, @args) = @_;
+ $self->on(test => sub { $self->requires(@args) });
+}
+
+sub author_requires {
+ my($self, @args) = @_;
+ $self->on(develop => sub { $self->requires(@args) });
+}
+
+1;
+
diff --git a/lib/Module/CPANfile/Prereq.pm b/lib/Module/CPANfile/Prereq.pm
new file mode 100644
index 0000000..cf675f1
--- /dev/null
+++ b/lib/Module/CPANfile/Prereq.pm
@@ -0,0 +1,21 @@
+package Module::CPANfile::Prereq;
+use strict;
+
+sub new {
+ my($class, %options) = @_;
+ bless \%options, $class;
+}
+
+sub feature { $_[0]->{feature} }
+sub phase { $_[0]->{phase} }
+sub type { $_[0]->{type} }
+sub module { $_[0]->{module} }
+sub requirement { $_[0]->{requirement} }
+
+sub match_feature {
+ my($self, $identifier) = @_;
+ no warnings 'uninitialized';
+ $self->feature eq $identifier;
+}
+
+1;
diff --git a/lib/Module/CPANfile/Prereqs.pm b/lib/Module/CPANfile/Prereqs.pm
new file mode 100644
index 0000000..c3126ea
--- /dev/null
+++ b/lib/Module/CPANfile/Prereqs.pm
@@ -0,0 +1,117 @@
+package Module::CPANfile::Prereqs;
+use strict;
+use Carp ();
+use CPAN::Meta::Feature;
+use Module::CPANfile::Prereq;
+
+sub from_cpan_meta {
+ my($class, $prereqs) = @_;
+
+ my $self = $class->new;
+
+ for my $phase (keys %$prereqs) {
+ for my $type (keys %{ $prereqs->{$phase} }) {
+ while (my($module, $requirement) = each %{ $prereqs->{$phase}{$type} }) {
+ $self->add_prereq(
+ phase => $phase,
+ type => $type,
+ module => $module,
+ requirement => Module::CPANfile::Requirement->new(name => $module, version => $requirement),
+ );
+ }
+ }
+ }
+
+ $self;
+}
+
+sub new {
+ my $class = shift;
+ bless {
+ prereqs => [],
+ features => {},
+ }, $class;
+}
+
+sub add_feature {
+ my($self, $identifier, $description) = @_;
+ $self->{features}{$identifier} = { description => $description };
+}
+
+sub add_prereq {
+ my($self, %args) = @_;
+ $self->add( Module::CPANfile::Prereq->new(%args) );
+}
+
+sub add {
+ my($self, $prereq) = @_;
+ push @{$self->{prereqs}}, $prereq;
+}
+
+sub as_cpan_meta {
+ my $self = shift;
+ $self->{cpanmeta} ||= $self->build_cpan_meta;
+}
+
+sub build_cpan_meta {
+ my($self, $identifier) = @_;
+
+ my $prereq_spec = {};
+ $self->prereq_each($identifier, sub {
+ my $prereq = shift;
+ $prereq_spec->{$prereq->phase}{$prereq->type}{$prereq->module} = $prereq->requirement->version;
+ });
+
+ CPAN::Meta::Prereqs->new($prereq_spec);
+}
+
+sub prereq_each {
+ my($self, $identifier, $code) = @_;
+
+ for my $prereq (@{$self->{prereqs}}) {
+ next unless $prereq->match_feature($identifier);
+ $code->($prereq);
+ }
+}
+
+sub merged_requirements {
+ my $self = shift;
+
+ my $reqs = CPAN::Meta::Requirements->new;
+ for my $prereq (@{$self->{prereqs}}) {
+ $reqs->add_string_requirement($prereq->module, $prereq->requirement->version);
+ }
+
+ $reqs;
+}
+
+sub find {
+ my($self, $module) = @_;
+
+ for my $prereq (@{$self->{prereqs}}) {
+ return $prereq if $prereq->module eq $module;
+ }
+
+ return;
+}
+
+sub identifiers {
+ my $self = shift;
+ keys %{$self->{features}};
+}
+
+sub feature {
+ my($self, $identifier) = @_;
+
+ my $data = $self->{features}{$identifier}
+ or Carp::croak("Unknown feature '$identifier'");
+
+ my $prereqs = $self->build_cpan_meta($identifier);
+
+ CPAN::Meta::Feature->new($identifier, {
+ description => $data->{description},
+ prereqs => $prereqs->as_string_hash,
+ });
+}
+
+1;
diff --git a/lib/Module/CPANfile/Requirement.pm b/lib/Module/CPANfile/Requirement.pm
new file mode 100644
index 0000000..01c6358
--- /dev/null
+++ b/lib/Module/CPANfile/Requirement.pm
@@ -0,0 +1,25 @@
+package Module::CPANfile::Requirement;
+use strict;
+
+sub new {
+ my ($class, %args) = @_;
+
+ $args{version} ||= 0;
+
+ bless +{
+ name => delete $args{name},
+ version => delete $args{version},
+ options => \%args,
+ }, $class;
+}
+
+sub name { $_[0]->{name} }
+sub version { $_[0]->{version} }
+
+sub options { $_[0]->{options} }
+
+sub has_options {
+ keys %{$_[0]->{options}} > 0;
+}
+
+1;