From: Yuval Kogman Date: Sun, 30 Apr 2006 11:33:39 +0000 (+0000) Subject: nothingmuch's take on delegation (no attr proxying yet) X-Git-Tag: 0_09_03~57 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=54b1cdf02dca36b7900abff23039fe0be898edb4;p=gitmo%2FMoose.git nothingmuch's take on delegation (no attr proxying yet) --- diff --git a/lib/Moose.pm b/lib/Moose.pm index 2121756..285340c 100644 --- a/lib/Moose.pm +++ b/lib/Moose.pm @@ -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(); + } + } } ); diff --git a/lib/Moose/Meta/Class.pm b/lib/Moose/Meta/Class.pm index 6889128..f5d211a 100644 --- a/lib/Moose/Meta/Class.pm +++ b/lib/Moose/Meta/Class.pm @@ -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 index 0000000..ab31491 --- /dev/null +++ b/t/070_delegation.t @@ -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)" );