diff options
Diffstat (limited to 'lib/Module/Implementation.pm')
-rw-r--r-- | lib/Module/Implementation.pm | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/lib/Module/Implementation.pm b/lib/Module/Implementation.pm new file mode 100644 index 0000000..0cdc3b5 --- /dev/null +++ b/lib/Module/Implementation.pm @@ -0,0 +1,290 @@ +package Module::Implementation; +# git description: v0.08-2-gd599347 +$Module::Implementation::VERSION = '0.09'; + +use strict; +use warnings; + +use Module::Runtime 0.012 qw( require_module ); +use Try::Tiny; + +# This is needed for the benefit of Test::CleanNamespaces, which in turn loads +# Package::Stash, which in turn loads this module and expects a minimum +# version. +unless ( exists $Module::Implementation::{VERSION} + && ${ $Module::Implementation::{VERSION} } ) { + + $Module::Implementation::{VERSION} = \42; +} + +my %Implementation; + +sub build_loader_sub { + my $caller = caller(); + + return _build_loader( $caller, @_ ); +} + +sub _build_loader { + my $package = shift; + my %args = @_; + + my @implementations = @{ $args{implementations} }; + my @symbols = @{ $args{symbols} || [] }; + + my $implementation; + my $env_var = uc $package; + $env_var =~ s/::/_/g; + $env_var .= '_IMPLEMENTATION'; + + return sub { + my ( $implementation, $loaded ) = _load_implementation( + $package, + $ENV{$env_var}, + \@implementations, + ); + + $Implementation{$package} = $implementation; + + _copy_symbols( $loaded, $package, \@symbols ); + + return $loaded; + }; +} + +sub implementation_for { + my $package = shift; + + return $Implementation{$package}; +} + +sub _load_implementation { + my $package = shift; + my $env_value = shift; + my $implementations = shift; + + if ($env_value) { + die "$env_value is not a valid implementation for $package" + unless grep { $_ eq $env_value } @{$implementations}; + + my $requested = "${package}::$env_value"; + + # Values from the %ENV hash are tainted. We know it's safe to untaint + # this value because the value was one of our known implementations. + ($requested) = $requested =~ /^(.+)$/; + + try { + require_module($requested); + } + catch { + require Carp; + Carp::croak("Could not load $requested: $_"); + }; + + return ( $env_value, $requested ); + } + else { + my $err; + for my $possible ( @{$implementations} ) { + my $try = "${package}::$possible"; + + my $ok; + try { + require_module($try); + $ok = 1; + } + catch { + $err .= $_ if defined $_; + }; + + return ( $possible, $try ) if $ok; + } + + require Carp; + if ( defined $err && length $err ) { + Carp::croak( + "Could not find a suitable $package implementation: $err"); + } + else { + Carp::croak( + 'Module::Runtime failed to load a module but did not throw a real error. This should never happen. Something is very broken' + ); + } + } +} + +sub _copy_symbols { + my $from_package = shift; + my $to_package = shift; + my $symbols = shift; + + for my $sym ( @{$symbols} ) { + my $type = $sym =~ s/^([\$\@\%\&\*])// ? $1 : '&'; + + my $from = "${from_package}::$sym"; + my $to = "${to_package}::$sym"; + + { + no strict 'refs'; + no warnings 'once'; + + # Copied from Exporter + *{$to} + = $type eq '&' ? \&{$from} + : $type eq '$' ? \${$from} + : $type eq '@' ? \@{$from} + : $type eq '%' ? \%{$from} + : $type eq '*' ? *{$from} + : die + "Can't copy symbol from $from_package to $to_package: $type$sym"; + } + } +} + +1; + +# ABSTRACT: Loads one of several alternate underlying implementations for a module + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Module::Implementation - Loads one of several alternate underlying implementations for a module + +=head1 VERSION + +version 0.09 + +=head1 SYNOPSIS + + package Foo::Bar; + + use Module::Implementation; + + BEGIN { + my $loader = Module::Implementation::build_loader_sub( + implementations => [ 'XS', 'PurePerl' ], + symbols => [ 'run', 'check' ], + ); + + $loader->(); + } + + package Consumer; + + # loads the first viable implementation + use Foo::Bar; + +=head1 DESCRIPTION + +This module abstracts out the process of choosing one of several underlying +implementations for a module. This can be used to provide XS and pure Perl +implementations of a module, or it could be used to load an implementation for +a given OS or any other case of needing to provide multiple implementations. + +This module is only useful when you know all the implementations ahead of +time. If you want to load arbitrary implementations then you probably want +something like a plugin system, not this module. + +=head1 API + +This module provides two subroutines, neither of which are exported. + +=head2 Module::Implementation::build_loader_sub(...) + +This subroutine takes the following arguments. + +=over 4 + +=item * implementations + +This should be an array reference of implementation names. Each name should +correspond to a module in the caller's namespace. + +In other words, using the example in the L</SYNOPSIS>, this module will look +for the C<Foo::Bar::XS> and C<Foo::Bar::PurePerl> modules. + +This argument is required. + +=item * symbols + +A list of symbols to copy from the implementation package to the calling +package. + +These can be prefixed with a variable type: C<$>, C<@>, C<%>, C<&>, or +C<*)>. If no prefix is given, the symbol is assumed to be a subroutine. + +This argument is optional. + +=back + +This subroutine I<returns> the implementation loader as a sub reference. + +It is up to you to call this loader sub in your code. + +I recommend that you I<do not> call this loader in an C<import()> sub. If a +caller explicitly requests no imports, your C<import()> sub will not be run at +all, which can cause weird breakage. + +=head2 Module::Implementation::implementation_for($package) + +Given a package name, this subroutine returns the implementation that was +loaded for the package. This is not a full package name, just the suffix that +identifies the implementation. For the L</SYNOPSIS> example, this subroutine +would be called as C<Module::Implementation::implementation_for('Foo::Bar')>, +and it would return "XS" or "PurePerl". + +=head1 HOW THE IMPLEMENTATION LOADER WORKS + +The implementation loader works like this ... + +First, it checks for an C<%ENV> var specifying the implementation to load. The +env var is based on the package name which loads the implementations. The +C<::> package separator is replaced with C<_>, and made entirely +upper-case. Finally, we append "_IMPLEMENTATION" to this name. + +So in our L</SYNOPSIS> example, the corresponding C<%ENV> key would be +C<FOO_BAR_IMPLEMENTATION>. + +If this is set, then the loader will B<only> try to load this one +implementation. + +If the env var requests an implementation which doesn't match one of the +implementations specified when the loader was created, an error is thrown. + +If this one implementation fails to load then loader throws an error. This is +useful for testing. You can request a specific implementation in a test file +by writing something like this: + + BEGIN { $ENV{FOO_BAR_IMPLEMENTATION} = 'XS' } + use Foo::Bar; + +If the environment variable is I<not> set, then the loader simply tries the +implementations originally passed to C<Module::Implementation>. The +implementations are tried in the order in which they were originally passed. + +The loader will use the first implementation that loads without an error. It +will copy any requested symbols from this implementation. + +If none of the implementations can be loaded, then the loader throws an +exception. + +The loader returns the name of the package it loaded. + +=head1 AUTHOR + +Dave Rolsky <autarch@urth.org> + +=head1 COPYRIGHT AND LICENSE + +This software is Copyright (c) 2014 by Dave Rolsky. + +This is free software, licensed under: + + The Artistic License 2.0 (GPL Compatible) + +=cut |