Sub-Methodical

 view release on metacpan or  search on metacpan

lib/Sub/Methodical.pm  view on Meta::CPAN

use strict;
use warnings;

package Sub::Methodical;

our $VERSION = '0.002';

my %methodical;
my %wrapped;
my %auto_methodical;

use B;
use PadWalker;
use Filter::EOF;
use Sub::Install ();
use Sub::Exporter -setup => {
  exports => [
    MODIFY_CODE_ATTRIBUTES => \&_build_MODIFY,
    AUTOLOAD => \&_build_AUTOLOAD,
  ],
  groups => {
    default => [qw(MODIFY_CODE_ATTRIBUTES)],
    inherit => [qw(AUTOLOAD)],
  },
  collectors => {
    -auto => sub {
      my ($col, $arg) = @_;
      $auto_methodical{$arg->{into}} = 1;
      push @{ $arg->{import_args} }, (
        [ 'MODIFY_CODE_ATTRIBUTES', undef ],
      );
    },
  },
};

sub _build_MODIFY {
  Filter::EOF->on_eof_call(sub {
    for my $pkg (keys %methodical) {
      for my $sub (@{ $methodical{$pkg} }) {
        _wrap($pkg, $sub);
      }
    }
    for my $pkg (keys %auto_methodical) {
      no strict 'refs';
      for my $subname (grep {
        !/^MODIFY_.+_ATTRIBUTES$/ &&
        $_ ne 'AUTOLOAD' &&
        !/^_/ &&
        *{$pkg . '::' . $_}{CODE}
      } keys %{$pkg . '::'}) {
        my $sub = \&{$pkg . '::' . $subname};
        next unless B::svref_2object($sub)->STASH->NAME eq $pkg;
        _wrap($pkg, $sub);
      }
    }
  });
  return sub {
    my ($pkg, $ref, @attrs) = @_;

    if (ref $ref eq 'CODE' and grep { $_ eq 'Methodical' } @attrs) {
      push @{ $methodical{$pkg} ||= [] }, $ref;
      @attrs = grep { $_ ne 'Methodical' } @attrs;
    }
    return @attrs;
  };
}

sub _build_AUTOLOAD {
  return sub {
    our $AUTOLOAD;
    my ($pkg, $method) = $AUTOLOAD =~ /^(.+)::(.+)$/;
    my ($wrap_pkg) = grep { $pkg->isa($_) && $wrapped{$_}{$method} }
      keys %wrapped;
    if ($wrap_pkg) {
      no strict 'refs';
      goto &{$wrap_pkg . '::' . $method};
    }
    require Carp;
    Carp::croak "Undefined subroutine &$AUTOLOAD called";
  };
}

sub _wrap {
  my ($pkg, $sub) = @_;
  require B;
  my $name = B::svref_2object($sub)->GV->NAME;
  (my $as = $name) =~ s/.*:://;
  #warn "wrapping $name ($pkg\::$as)\n";
  $wrapped{$pkg}{$as} = $sub;
  Sub::Install::reinstall_sub({
    into => $pkg,
    as   => $as,
    code => sub {
      if (eval { $_[0]->isa($pkg) }) {
        #warn "calling $name directly: @_\n";
        return $sub->(@_);
      }
      my $pad = PadWalker::peek_my(1);
      my $self = $pad->{'$self'};
      unless ($self) {
        die "can't find \$self!";
      }
      unless (eval { $$self->isa($pkg) }) {
        require Carp;
        Carp::croak sprintf 
          "Methodical '%s' called with incorrect invocant '%s' (wanted '%s')",
          $as, $$self, $pkg;
      }
      #warn "calling $name with self = $$self, @_\n";
      $$self->$as(@_);
    },
  });
}

1;
__END__

=head1 NAME

Sub::Methodical - call methods as functions

=head1 VERSION

Version 0.002

=head1 SYNOPSIS

  package My::Module;
  use Sub::Methodical;

  sub foo :Methodical { ... do stuff ... }

  sub bar {
    my ($self, $arg) = @_;
    
    # this secretly grabs the current scope's $self
    foo($arg, @more_args);

    # ... this is identical
    $self->foo($arg, @more_args);
  }

=head1 DESCRIPTION

Don't you get tired of typing C<< $self-> >> all the time when you're calling
your methods?

Now you don't have to anymore.  Any function you give the C<:Methodical>
attribute (or, with the C<-auto> import argument, any function that doesn't
start with '_') is automatically called as a method whenever you call it as a
function, taking its invocant (C<$self>) from the calling scope.

=head1 USE

=head2 The C<:Methodical> Attribute

This attribute marks a single function as a Methodical method.  Once marked,
these two invocations are identical:



( run in 1.552 second using v1.01-cache-2.11-cpan-39bf76dae61 )