package Class::Meta::Method;

=head1 NAME

Class::Meta::Method - Class::Meta class method introspection

=head1 SYNOPSIS

  # Assuming MyApp::Thingy was generated by Class::Meta.
  my $class = MyApp::Thingy->my_class;
  my $thingy = MyApp::Thingy->new;

  print "\nMethods:\n";
  for my $meth ($class->methods) {
      print "  o ", $meth->name, $/;
      $meth->call($thingy);
  }

=head1 DESCRIPTION

This class provides an interface to the C<Class::Meta> objects that describe
methods. It supports a simple description of the method, a label, and its
visibility (private, protected, trusted, or public).

Class::Meta::Method objects are created by Class::Meta; they are never
instantiated directly in client code. To access the method objects for a
Class::Meta-generated class, simply call its C<my_class()> method to retrieve
its Class::Meta::Class object, and then call the C<methods()> method on the
Class::Meta::Class object.

=cut

##############################################################################
# Dependencies                                                               #
##############################################################################
use strict;

##############################################################################
# Package Globals                                                            #
##############################################################################
our $VERSION = '0.66';

=head1 INTERFACE

=head2 Constructors

=head3 new

A protected method for constructing a Class::Meta::Method object. Do not call
this method directly; Call the L<C<add_method()>|Class::Meta/"add_method">
method on a Class::Meta object, instead.

=cut

sub new {
    my $pkg = shift;
    my $class = shift;

    # Check to make sure that only Class::Meta or a subclass is constructing a
    # Class::Meta::Method object.
    my $caller = caller;
    Class::Meta->handle_error("Package '$caller' cannot create $pkg "
                              . "objects")
      unless UNIVERSAL::isa($caller, 'Class::Meta')
        || UNIVERSAL::isa($caller, __PACKAGE__);

    # Make sure we can get all the arguments.
    $class->handle_error("Odd number of parameters in call to new() "
                                 . "when named parameters were expected")
      if @_ % 2;

    my %p = @_;

    # Validate the name.
    $class->handle_error("Parameter 'name' is required in call to "
                                 . "new()") unless $p{name};
    $class->handle_error("Method '$p{name}' is not a valid method "
             . "name -- only alphanumeric and '_' characters allowed")
      if $p{name} =~ /\W/;

    # Make sure the name hasn't already been used for another method
    # or constructor.
    $class->handle_error("Method '$p{name}' already exists in class "
             . "'$class->{package}'")
      if exists $class->{meths}{$p{name}}
      || exists $class->{ctors}{$p{name}};

    # Check the visibility.
    if (exists $p{view}) {
        $p{view} = Class::Meta::_str_to_const($p{view});
        $class->handle_error("Not a valid view parameter: '$p{view}'")
          unless $p{view} == Class::Meta::PUBLIC
          ||     $p{view} == Class::Meta::PROTECTED
          ||     $p{view} == Class::Meta::TRUSTED
          ||     $p{view} == Class::Meta::PRIVATE;
    } else {
        # Make it public by default.
        $p{view} = Class::Meta::PUBLIC;
    }

    # Check the context.
    if (exists $p{context}) {
        $p{context} = Class::Meta::_str_to_const($p{context});
        $class->handle_error("Not a valid context parameter: "
                                     . "'$p{context}'")
          unless $p{context} == Class::Meta::OBJECT
          ||     $p{context} == Class::Meta::CLASS;
    } else {
        # Make it public by default.
        $p{context} = Class::Meta::OBJECT;
    }

    # Validate or create the method caller if necessary.
    if ($p{caller}) {
        my $ref = ref $p{caller};
        $class->handle_error(
            'Parameter caller must be a code reference'
        ) unless $ref && $ref eq 'CODE'
    } else {
        $p{caller} = eval "sub { shift->$p{name}(\@_) }"
            if $p{view} > Class::Meta::PRIVATE;
    }

    if ($p{code}) {
        my $ref = ref $p{code};
        $class->handle_error(
            'Parameter code must be a code reference'
        ) unless $ref && $ref eq 'CODE';
    }

    # Create and cache the method object.
    $p{package} = $class->{package};
    $class->{meths}{$p{name}} = bless \%p, ref $pkg || $pkg;

    # Index its view.
    push @{ $class->{all_meth_ord} }, $p{name};
    if ($p{view} > Class::Meta::PRIVATE) {
        push @{$class->{prot_meth_ord}}, $p{name}
          unless $p{view} == Class::Meta::TRUSTED;
        if ($p{view} > Class::Meta::PROTECTED) {
            push @{$class->{trst_meth_ord}}, $p{name};
            push @{$class->{meth_ord}}, $p{name}
              if $p{view} == Class::Meta::PUBLIC;
        }
    }

    # Store a reference to the class object.
    $p{class} = $class;

    # Let 'em have it.
    return $class->{meths}{$p{name}};
}

##############################################################################
# Instance Methods                                                           #
##############################################################################

=head2 Instance Methods

=head3 name

  my $name = $meth->name;

Returns the method name.

=head3 package

  my $package = $meth->package;

Returns the method package.

=head3 desc

  my $desc = $meth->desc;

Returns the description of the method.

=head3 label

  my $desc = $meth->label;

Returns label for the method.

=head3 view

  my $view = $meth->view;

Returns the view of the method, reflecting its visibility. The possible
values are defined by the following constants:

=over 4

=item Class::Meta::PUBLIC

=item Class::Meta::PRIVATE

=item Class::Meta::TRUSTED

=item Class::Meta::PROTECTED

=back

=head3 context

  my $context = $meth->context;

Returns the context of the method, essentially whether it is a class or
object method. The possible values are defined by the following constants:

=over 4

=item Class::Meta::CLASS

=item Class::Meta::OBJECT

=back

=head3 args

A description of the arguments to the method. This can be anything you like,
but I recommend something like a string for a single argument, an array
reference for a list of arguments, or a hash reference for parameter
arguments.

=head3 returns

A description of the return value or values of the method.

=head3 class

  my $class = $meth->class;

Returns the Class::Meta::Class object that this method is associated
with. Note that this object will always represent the class in which the
method is defined, and I<not> any of its subclasses.

=cut

sub name    { $_[0]->{name}    }
sub package { $_[0]->{package} }
sub desc    { $_[0]->{desc}    }
sub label   { $_[0]->{label}   }
sub view    { $_[0]->{view}    }
sub context { $_[0]->{context} }
sub args    { $_[0]->{args}    }
sub returns { $_[0]->{returns} }
sub class   { $_[0]->{class}   }

=head3 call

  my $ret = $meth->call($obj, @args);

Calls the method on the C<$obj> object, passing in any arguments. Note that it
uses a C<goto> to execute the method, so the call to C<call()> itself will not
appear in a call stack trace.

=cut

sub call {
    my $self = shift;
    my $code = $self->{caller}
      or $self->class->handle_error("Cannot call method '", $self->name, "'");
    goto &$code;
}

##############################################################################

=head3 build

  $meth->build($class);

This is a protected method, designed to be called only by the Class::Meta
class or a subclass of Class::Meta. It takes a single argument, the
Class::Meta::Class object for the class in which the method was defined. Once
it checks to make sure that it is only called by Class::Meta or a subclass of
Class::Meta or of Class::Meta::Method, C<build()> installs the method if it
was specified via the C<code> parameter to C<new()>.

Although you should never call this method directly, subclasses of
Class::Meta::Method may need to override it in order to add behavior.

=cut

sub build {
    my ($self, $class) = @_;

    # Check to make sure that only Class::Meta or a subclass is building
    # methods.
    my $caller = caller;
    $self->class->handle_error(
        "Package '$caller' cannot call " . ref($self) . "->build"
    ) unless UNIVERSAL::isa($caller, 'Class::Meta')
        || UNIVERSAL::isa($caller, __PACKAGE__);

    # Install the method if we've got it.
    if (my $code = delete $self->{code}) {
        my $pack = $self->package;
        my $name = $self->{name};
        if ($self->{view} < Class::Meta::PUBLIC ) {
            # Add a constraint to the code ref.
            my $real_meth = $code;
            if ($self->{view} == Class::Meta::PROTECTED) {
                $code = sub {
                    $self->class->handle_error(
                        "$name is a protected method of $pack"
                    ) unless UNIVERSAL::isa(scalar caller, $pack);
                    goto &$real_meth;
                };
            } elsif ($self->{view} == Class::Meta::PRIVATE) {
                $code = sub {
                    $self->class->handle_error(
                        "$name is a private method of $pack"
                    ) unless caller eq $pack;
                    goto &$real_meth;
                };
            } elsif ($self->{view} == Class::Meta::TRUSTED) {
                my $trusted = $self->class->trusted;
                $code = sub {
                    my $caller = caller;
                    goto &$real_meth if $caller eq $pack;
                    for my $pkg ( @{ $trusted } ) {
                        goto &$real_meth if UNIVERSAL::isa($caller, $pkg);
                    }
                    $self->class->handle_error(
                        "$name is a trusted method of $pack"
                    );
                };
            }
        }

        no strict 'refs';
        *{"$pack\::$name"} = $code;
    }

    return $self;
}

1;
__END__

=head1 SUPPORT

This module is stored in an open L<GitHub
repository|http://github.com/theory/class-meta/>. Feel free to fork and
contribute!

Please file bug reports via L<GitHub
Issues|http://github.com/theory/class-meta/issues/> or by sending mail to
L<bug-Class-Meta@rt.cpan.org|mailto:bug-Class-Meta@rt.cpan.org>.

=head1 AUTHOR

David E. Wheeler <david@justatheory.com>

=head1 SEE ALSO

Other classes of interest within the Class::Meta distribution include:

=over 4

=item L<Class::Meta|Class::Meta>

=item L<Class::Meta::Class|Class::Meta::Class>

=item L<Class::Meta::Constructor|Class::Meta::Constructor>

=item L<Class::Meta::Attribute|Class::Meta::Attribute>

=back

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2002-2011, David E. Wheeler. Some Rights Reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.

=cut
