nothingmuch's take on delegation (no attr proxying yet)
Yuval Kogman [Sun, 30 Apr 2006 11:33:39 +0000 (11:33 +0000)]
lib/Moose.pm
lib/Moose/Meta/Class.pm
t/070_delegation.t [new file with mode: 0644]

index 2121756..285340c 100644 (file)
@@ -145,6 +145,14 @@ use Moose::Util::TypeConstraints;
         },
         blessed => sub {
             return \&Scalar::Util::blessed;
+        },
+        all_methods => sub {
+            sub () {
+                sub {
+                    my ( $class, $delegate_class ) = @_;
+                    $delegate_class->compute_all_applicable_methods();
+                }
+            }
         }
     );
 
index 6889128..f5d211a 100644 (file)
@@ -7,7 +7,7 @@ use warnings;
 use Class::MOP;
 
 use Carp         'confess';
-use Scalar::Util 'weaken', 'blessed';
+use Scalar::Util 'weaken', 'blessed', 'reftype';
 
 our $VERSION = '0.05';
 
@@ -79,6 +79,100 @@ sub has_method {
     return $self->SUPER::has_method($method_name);    
 }
 
+sub add_attribute {
+    my ($self, $name, %params) = @_;
+
+    my @delegations;
+    if ( my $delegation = delete $params{handles} ) {
+        my @method_names_or_hashes = $self->compute_delegation( $name, $delegation, \%params );
+        @delegations = $self->get_delegatable_methods( @method_names_or_hashes );
+    }
+
+    my $ret = $self->SUPER::add_attribute( $name, %params );
+
+    if ( @delegations ) {
+        my $attr = $self->get_attribute( $name );
+        $self->generate_delgate_method( $attr, $_ ) for @delegations;
+    }
+
+    return $ret;
+}
+
+sub generate_delgate_method {
+    my ( $self, $attr, $method ) = @_;
+
+    # FIXME like generated accessors these methods must be regenerated
+    # FIXME the reader may not work for subclasses with weird instances
+
+    my $reader = $attr->generate_reader_method( $attr->name ); # FIXME no need for attr name
+
+    my $method_name = $method->{name};
+    my $new_name = $method->{new_name} || $method_name;
+
+    $self->add_method( $new_name, sub {
+        if ( Scalar::Util::blessed( my $delegate = shift->$reader ) ) {
+            return $delegate->$method_name( @_ );
+        }
+        return;
+    });
+}
+
+sub compute_delegation {
+    my ( $self, $attr_name, $delegation, $params ) = @_;
+
+   
+    # either it's a concrete list of method names
+    return $delegation unless ref $delegation; # single method name
+    return @$delegation if reftype($delegation) eq "ARRAY";
+
+    # or it's a generative api
+    my $delegator_meta = $self->_guess_attr_class_or_role( $attr_name, $params );
+    $self->generate_delegation_list( $delegation, $delegator_meta );
+}
+
+sub get_delegatable_methods {
+    my ( $self, @names_or_hashes ) = @_;
+    my @hashes = map { ref($_) ? $_ : { name => $_ } } @names_or_hashes;
+    return grep { !$self->name->can( $_->{name} ) } @hashes;
+}
+
+sub generate_delegation_list {
+    my ( $self, $delegation, $delegator_meta ) = @_;
+
+    if ( reftype($delegation) eq "CODE" ) {
+        return $delegation->( $self, $delegator_meta );
+    } elsif ( blessed($delegation) eq "Regexp" ) {
+        return grep { $_->{name} =~ /$delegation/ } $delegator_meta->compute_all_applicable_methods();
+    } else {
+        confess "The 'handles' specification '$delegation' is not supported";
+    }
+}
+
+sub _guess_attr_class_or_role {
+    my ( $self, $attr, $params ) = @_;
+
+    my ( $isa, $does ) = @{ $params }{qw/isa does/};
+
+    confess "Generative delegations must explicitly specify a class or a role for the attribute's type"
+        unless $isa || $does;
+
+    # if it's a class/role name make it into a meta object
+    for (grep { defined && !ref($_) } $isa, $does) {
+        confess "Generative delegations must refer to Moose class/role types"
+            unless $_->can("meta");
+        $_ = $_->meta;
+    }
+
+    for (grep { blessed($_) } $isa, $does) {
+        confess "You must use classes/roles, not type constraints to use delegation"
+            unless $_->isa( "Moose::Meta::Class" );
+    }
+    
+    confess "Cannot have an isa option and a does option if the isa does not do the does"
+        if $isa && $does and !confess->does( $does );
+
+    return $isa || $does;
+}
 
 sub add_override_method_modifier {
     my ($self, $name, $method, $_super_package) = @_;
diff --git a/t/070_delegation.t b/t/070_delegation.t
new file mode 100644 (file)
index 0000000..ab31491
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More tests => 34;
+use Test::Exception;
+
+{
+
+    package ChildASuper;
+    use Moose;
+
+    sub child_a_super_method { "as" }
+
+    package ChildA;
+    use Moose;
+
+    extends "ChildASuper";
+
+    sub child_a_method_1 { "a1" }
+    sub child_a_method_2 { Scalar::Util::blessed($_[0]) . " a2" }
+
+    package ChildASub;
+    use Moose;
+
+    extends "ChildA";
+
+    sub child_a_method_3 { "a3" }
+
+    package ChildB;
+    use Moose;
+
+    sub child_b_method_1 { "b1" }
+    sub child_b_method_2 { "b2" }
+    sub child_b_method_3 { "b3" }
+
+    package ChildC;
+    use Moose;
+
+    sub child_c_method_1 { "c1" }
+    sub child_c_method_2 { "c2" }
+    sub child_c_method_3_la { "c3" }
+    sub child_c_method_4_la { "c4" }
+
+    package ChildD;
+    use Moose;
+
+    sub child_d_method_1 { "d1" }
+    sub child_d_method_2 { "d2" }
+
+    package ChildE;
+    # no Moose
+
+    sub new { bless {}, shift }
+    sub child_e_method_1 { "e1" }
+    sub child_e_method_2 { "e2" }
+
+    package ChildF;
+    # no Moose
+
+    sub new { bless {}, shift }
+    sub child_f_method_1 { "f1" }
+    sub child_f_method_2 { "f2" }
+
+    package Parent;
+    use Moose;
+
+    ::dies_ok {
+        has child_a => (
+            is      => "ro",
+            default => sub { ChildA->new },
+            handles => all_methods,
+        );
+    } "all_methods requires explicit isa";
+
+    ::lives_ok {
+        has child_a => (
+            isa     => "ChildA",
+            is      => "ro",
+            default => sub { ChildA->new },
+            handles => all_methods,
+        );
+    } "allow all_methods with explicit isa";
+
+    ::lives_ok {
+        has child_b => (
+            default => sub { ChildB->new },
+            handles => [qw/child_b_method_1/],
+        );
+    } "don't need to declare isa if method list is predefined";
+
+    ::lives_ok {
+        has child_c => (
+            isa     => "ChildC",
+            is      => "ro",
+            default => sub { ChildC->new },
+            handles => qr/_la$/,
+        );
+    } "can declare regex collector";
+
+    ::dies_ok {
+        has child_d => (
+            is      => "ro",
+            default => sub { ChildD->new },
+            handles => sub {
+                my ( $class, $delegate_class ) = @_;
+            }
+        );
+    } "can't create attr with generative handles parameter and no isa";
+
+    ::lives_ok {
+        has child_d => (
+            isa     => "ChildD",
+            is      => "ro",
+            default => sub { ChildD->new },
+            handles => sub {
+                my ( $class, $delegate_class ) = @_;
+                return;
+            }
+        );
+    } "can't create attr with generative handles parameter and no isa";
+
+    ::lives_ok {
+        has child_e => (
+            isa     => "ChildE",
+            is      => "ro",
+            default => sub { ChildE->new },
+            handles => "child_e_method_2",
+        );
+    } "can delegate to non moose class using explicit method list";
+
+    ::dies_ok {
+        has child_f => (
+            isa     => "ChildF",
+            is      => "ro",
+            default => sub { ChildF->new },
+            handles => sub { },
+        );
+    } "but not generative one";
+
+    sub parent_method { "p" }
+}
+
+# sanity
+
+isa_ok( my $p = Parent->new, "Parent" );
+isa_ok( $p->child_a, "ChildA" );
+#isa_ok( $p->child_b, "ChildB" ); # no accessor
+isa_ok( $p->child_c, "ChildC" );
+isa_ok( $p->child_d, "ChildD" );
+isa_ok( $p->child_e, "ChildE" );
+
+ok( !$p->can("child_b"), "no child b accessor" );
+ok( !$p->can("child_f"), "no child f" );
+
+is( $p->parent_method, "p", "parent method" );
+is( $p->child_a->child_a_super_method, "as", "child supermethod" );
+is( $p->child_a->child_a_method_1, "a1", "child method" );
+
+can_ok( $p, "child_a_super_method" );
+can_ok( $p, "child_a_method_1" );
+can_ok( $p, "child_a_method_2" );
+ok( !$p->can( "child_a_method_3" ), "but not subclass of delegate class" );
+
+is( $p->child_a_method_1, $p->child_a->child_a_method_1, "delegate behaves the same" );
+is( $p->child_a_method_2, "ChildA a2", "delegates are their own invocants" );
+
+
+can_ok( $p, "child_b_method_1" );
+ok( !$p->can("child_b_method_2"), "but not ChildB's unspecified siblings" );
+
+
+ok( !$p->can($_), "none of ChildD's methods ($_)" )
+    for grep { /^child/ } map { $_->{name} } ChildD->meta->compute_all_applicable_methods();
+
+can_ok( $p, "child_c_method_3_la" );
+can_ok( $p, "child_c_method_4_la" );
+
+is( $p->child_c_method_3_la, "c3", "ChildC method delegated OK" );
+
+can_ok( $p, "child_e_method_2" );
+ok( !$p->can("child_e_method_1"), "but not child_e_method_1");
+
+is( $p->child_e_method_2, "e2", "delegate to non moose class (child_e_method_2)" );