Implemented Moose::Util::MetaRole, which lets you apply roles to any
Dave Rolsky [Mon, 25 Aug 2008 15:27:43 +0000 (15:27 +0000)]
meta class, as well as the constructor & destructor classes and the
object base class.

To make this work properly, I had to make constructor & destructor
class attributes of Moose::Meta::Class, rather than just hardcoding
them.

lib/Moose/Meta/Class.pm
lib/Moose/Util/MetaRole.pm [new file with mode: 0644]
t/050_metaclasses/015_metarole.t [new file with mode: 0644]
t/050_metaclasses/016_metarole_w_metaclass_pm.t [new file with mode: 0644]

index 42e842d..f733a9e 100644 (file)
@@ -23,6 +23,16 @@ __PACKAGE__->meta->add_attribute('roles' => (
     default => sub { [] }
 ));
 
+__PACKAGE__->meta->add_attribute('constructor_class' => (
+    accessor => 'constructor_class',
+    default  => sub { 'Moose::Meta::Method::Constructor' }
+));
+
+__PACKAGE__->meta->add_attribute('destructor_class' => (
+    accessor => 'destructor_class',
+    default  => sub { 'Moose::Meta::Method::Destructor' }
+));
+
 sub initialize {
     my $class = shift;
     my $pkg   = shift;
@@ -424,8 +434,8 @@ sub make_immutable {
     my $self = shift;
     $self->SUPER::make_immutable
       (
-       constructor_class => 'Moose::Meta::Method::Constructor',
-       destructor_class  => 'Moose::Meta::Method::Destructor',
+       constructor_class => $self->constructor_class,
+       destructor_class  => $self->destructor_class,
        inline_destructor => 1,
        # NOTE:
        # no need to do this,
diff --git a/lib/Moose/Util/MetaRole.pm b/lib/Moose/Util/MetaRole.pm
new file mode 100644 (file)
index 0000000..d86b4aa
--- /dev/null
@@ -0,0 +1,96 @@
+package Moose::Util::MetaRole;
+
+use strict;
+use warnings;
+
+use List::MoreUtils qw( all );
+
+sub apply_metaclass_roles {
+    my %options = @_;
+
+    my $for = $options{for_class};
+
+    my $meta = _make_new_metaclass( $for, \%options );
+
+    for my $tor_class ( grep { $options{ $_ . '_roles' } }
+        qw( constructor_class destructor_class ) ) {
+
+        my $class = _make_new_class(
+            $meta->$tor_class(),
+            $options{ $tor_class . '_roles' }
+        );
+
+        $meta->$tor_class($class);
+    }
+
+    return $meta;
+}
+
+sub _make_new_metaclass {
+    my $for     = shift;
+    my $options = shift;
+
+    return $for->meta()
+        unless grep { exists $options->{ $_ . '_roles' } }
+            qw(
+            metaclass
+            attribute_metaclass
+            method_metaclass
+            instance_metaclass
+    );
+
+    my $new_metaclass
+        = _make_new_class( ref $for->meta(), $options->{metaclass_roles} );
+
+    my $old_meta = $for->meta();
+
+    Class::MOP::remove_metaclass_by_name($for);
+
+    my %classes = map {
+        $_ => _make_new_class( $old_meta->$_(), $options->{ $_ . '_roles' } )
+        } qw(
+        attribute_metaclass
+        method_metaclass
+        instance_metaclass
+    );
+
+    return $new_metaclass->reinitialize( $for, %classes );
+}
+
+sub apply_base_class_roles {
+    my %options = @_;
+
+    my $for = $options{for_class};
+
+    my $meta = $for->meta();
+
+    my $new_base = _make_new_class(
+        $for,
+        $options{roles},
+        [ $meta->superclasses() ],
+    );
+
+    $meta->superclasses($new_base)
+        if $new_base ne $meta->name();
+}
+
+sub _make_new_class {
+    my $existing_class = shift;
+    my $roles          = shift;
+    my $superclasses   = shift || [$existing_class];
+
+    return $existing_class unless $roles;
+
+    my $meta = $existing_class->meta();
+
+    return $existing_class
+        if $meta->can('does_role') && all { $meta->does_role($_) } @{$roles};
+
+    return Moose::Meta::Class->create_anon_class(
+        superclasses => $superclasses,
+        roles        => $roles,
+        cache        => 1,
+    )->name();
+}
+
+1;
diff --git a/t/050_metaclasses/015_metarole.t b/t/050_metaclasses/015_metarole.t
new file mode 100644 (file)
index 0000000..45244d1
--- /dev/null
@@ -0,0 +1,267 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More 'no_plan';
+
+use Moose::Util::MetaRole;
+
+
+{
+    package My::Meta::Class;
+    use Moose;
+    extends 'Moose::Meta::Class';
+}
+
+{
+    package My::Meta::Attribute;
+    use Moose;
+    extends 'Moose::Meta::Attribute';
+}
+
+{
+    package My::Meta::Method;
+    use Moose;
+    extends 'Moose::Meta::Method';
+}
+
+{
+    package My::Meta::Instance;
+    use Moose;
+    extends 'Moose::Meta::Instance';
+}
+
+{
+    package My::Meta::MethodConstructor;
+    use Moose;
+    extends 'Moose::Meta::Method::Constructor';
+}
+
+{
+    package My::Meta::MethodDestructor;
+    use Moose;
+    extends 'Moose::Meta::Method::Destructor';
+}
+
+{
+    package Role::Foo;
+    use Moose::Role;
+    has 'foo' => ( is => 'ro', default => 10 );
+}
+
+{
+    package My::Class;
+
+    use Moose;
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class       => 'My::Class',
+        metaclass_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        'apply Role::Foo to My::Class->meta()' );
+    is( My::Class->meta()->foo(), 10,
+        '... and call foo() on that meta object' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class                 => 'My::Class',
+        attribute_metaclass_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class->meta()'s attribute metaclass} );
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        '... My::Class->meta() still does Role::Foo' );
+
+    My::Class->meta()->add_attribute( 'size', is => 'ro' );
+    is( My::Class->meta()->get_attribute('size')->foo(), 10,
+        '... call foo() on an attribute metaclass object' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class              => 'My::Class',
+        method_metaclass_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class->meta()'s method metaclass} );
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        '... My::Class->meta() still does Role::Foo' );
+    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
+
+    My::Class->meta()->add_method( 'bar' => sub { 'bar' } );
+    is( My::Class->meta()->get_method('bar')->foo(), 10,
+        '... call foo() on a method metaclass object' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class              => 'My::Class',
+        instance_metaclass_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class->meta()'s instance metaclass} );
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        '... My::Class->meta() still does Role::Foo' );
+    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
+    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s method metaclass still does Role::Foo} );
+
+    is( My::Class->meta()->get_meta_instance()->foo(), 10,
+        '... call foo() on an instance metaclass object' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class               => 'My::Class',
+        constructor_class_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->constructor_class()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class->meta()'s constructor class} );
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        '... My::Class->meta() still does Role::Foo' );
+    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
+    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s method metaclass still does Role::Foo} );
+    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s instance metaclass still does Role::Foo} );
+
+    # Actually instantiating the constructor class is too freaking hard!
+    ok( My::Class->meta()->constructor_class()->can('foo'),
+        '... constructor class has a foo method' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class              => 'My::Class',
+        destructor_class_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->destructor_class()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class->meta()'s destructor class} );
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        '... My::Class->meta() still does Role::Foo' );
+    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
+    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s method metaclass still does Role::Foo} );
+    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s instance metaclass still does Role::Foo} );
+    ok( My::Class->meta()->constructor_class()->meta()->does_role('Role::Foo'),
+        q{... My::Class->meta()'s constructor class still does Role::Foo} );
+
+    # same problem as the constructor class
+    ok( My::Class->meta()->destructor_class()->can('foo'),
+        '... destructor class has a foo method' );
+}
+
+{
+    Moose::Util::MetaRole::apply_base_class_roles(
+        for_class => 'My::Class',
+        roles     => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->does_role('Role::Foo'),
+        'apply Role::Foo to My::Class base class' );
+    is( My::Class->new()->foo(), 10,
+        '... call foo() on a My::Class object' );
+}
+
+{
+    package My::Class2;
+
+    use Moose;
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class                 => 'My::Class2',
+        metaclass_roles           => ['Role::Foo'],
+        attribute_metaclass_roles => ['Role::Foo'],
+        method_metaclass_roles    => ['Role::Foo'],
+        instance_metaclass_roles  => ['Role::Foo'],
+        constructor_class_roles   => ['Role::Foo'],
+        destructor_class_roles    => ['Role::Foo'],
+    );
+
+    ok( My::Class2->meta()->meta()->does_role('Role::Foo'),
+        'apply Role::Foo to My::Class2->meta()' );
+    is( My::Class2->meta()->foo(), 10,
+        '... and call foo() on that meta object' );
+    ok( My::Class2->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s attribute metaclass} );
+    My::Class2->meta()->add_attribute( 'size', is => 'ro' );
+
+    is( My::Class2->meta()->get_attribute('size')->foo(), 10,
+        '... call foo() on an attribute metaclass object' );
+
+    ok( My::Class2->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s method metaclass} );
+
+    My::Class2->meta()->add_method( 'bar' => sub { 'bar' } );
+    is( My::Class2->meta()->get_method('bar')->foo(), 10,
+        '... call foo() on a method metaclass object' );
+
+    ok( My::Class2->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s instance metaclass} );
+    is( My::Class2->meta()->get_meta_instance()->foo(), 10,
+        '... call foo() on an instance metaclass object' );
+
+    ok( My::Class2->meta()->constructor_class()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s constructor class} );
+    ok( My::Class2->meta()->constructor_class()->can('foo'),
+        '... constructor class has a foo method' );
+
+    ok( My::Class2->meta()->destructor_class()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s destructor class} );
+    ok( My::Class2->meta()->destructor_class()->can('foo'),
+        '... destructor class has a foo method' );
+}
+
+
+{
+    package My::Meta;
+
+    use Moose::Exporter;
+    Moose::Exporter->setup_import_methods( also => 'Moose' );
+
+    sub init_meta {
+        shift;
+        my %p = @_;
+
+        Moose->init_meta( %p, metaclass => 'My::Meta::Class' );
+    }
+}
+
+{
+    package My::Class3;
+
+    My::Meta->import();
+}
+
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class                 => 'My::Class3',
+        metaclass_roles           => ['Role::Foo'],
+    );
+
+    ok( My::Class3->meta()->meta()->does_role('Role::Foo'),
+        'apply Role::Foo to My::Class3->meta()' );
+    is( My::Class3->meta()->foo(), 10,
+        '... and call foo() on that meta object' );
+    ok( ( grep { $_ eq 'My::Meta::Class' } My::Class3->meta()->meta()->superclasses() ),
+        'apply_metaclass_roles() does not interfere with metaclass set via Moose->init_meta()' );
+}
diff --git a/t/050_metaclasses/016_metarole_w_metaclass_pm.t b/t/050_metaclasses/016_metarole_w_metaclass_pm.t
new file mode 100644 (file)
index 0000000..8b5f2a0
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More 'no_plan';
+
+use Moose::Util::MetaRole;
+
+BEGIN
+{
+    package My::Meta::Class;
+    use Moose;
+    extends 'Moose::Meta::Class';
+}
+
+BEGIN
+{
+    package My::Meta::Attribute;
+    use Moose;
+    extends 'Moose::Meta::Attribute';
+}
+
+BEGIN
+{
+    package My::Meta::Method;
+    use Moose;
+    extends 'Moose::Meta::Method';
+}
+
+BEGIN
+{
+    package My::Meta::Instance;
+    use Moose;
+    extends 'Moose::Meta::Instance';
+}
+
+BEGIN
+{
+    package Role::Foo;
+    use Moose::Role;
+    has 'foo' => ( is => 'ro', default => 10 );
+}
+
+{
+    package My::Class;
+
+    use metaclass 'My::Meta::Class';
+    use Moose;
+}
+
+{
+    package My::Class2;
+
+    use metaclass 'My::Meta::Class' => (
+        attribute_metaclass => 'My::Meta::Attribute',
+        method_metaclass    => 'My::Meta::Method',
+        instance_metaclass  => 'My::Meta::Instance',
+    );
+
+    use Moose;
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class       => 'My::Class',
+        metaclass_roles => ['Role::Foo'],
+    );
+
+    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
+        'apply Role::Foo to My::Class->meta()' );
+    has_superclass( My::Class->meta(), 'My::Meta::Class',
+                    'apply_metaclass_roles works with metaclass.pm' );
+}
+
+{
+    Moose::Util::MetaRole::apply_metaclass_roles(
+        for_class                 => 'My::Class2',
+        attribute_metaclass_roles => ['Role::Foo'],
+        method_metaclass_roles    => ['Role::Foo'],
+        instance_metaclass_roles  => ['Role::Foo'],
+    );
+
+    ok( My::Class2->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s attribute metaclass} );
+    has_superclass( My::Class2->meta()->attribute_metaclass(), 'My::Meta::Attribute',
+                    '... and this does not interfere with attribute metaclass set via metaclass.pm' );
+    ok( My::Class2->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s method metaclass} );
+    has_superclass( My::Class2->meta()->method_metaclass(), 'My::Meta::Method',
+                    '... and this does not interfere with method metaclass set via metaclass.pm' );
+    ok( My::Class2->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
+        q{apply Role::Foo to My::Class2->meta()'s instance metaclass} );
+    has_superclass( My::Class2->meta()->instance_metaclass(), 'My::Meta::Instance',
+                    '... and this does not interfere with instance metaclass set via metaclass.pm' );
+}
+
+# like isa_ok but works with a class name, not just refs
+sub has_superclass {
+    my $thing  = shift;
+    my $parent = shift;
+    my $desc   = shift;
+
+    my %supers = map { $_ => 1 } $thing->meta()->superclasses();
+
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    ok( $supers{$parent}, $desc );
+}