#!/usr/bin/perl use strict; use warnings; use Test::More tests => 13; ## ---------------------------------------------------------------------------- ## Mini Meta-Model with Self-Bootstrapping Roles ## ---------------------------------------------------------------------------- ## This is an extension of the Mini-MetaModel which adds a self-bootstrapping ## implementation of Roles into the model. See below for more details. ## ---------------------------------------------------------------------------- { use Hash::Util 'lock_keys'; # Every instance should have a unique ID my $instance_counter = 0; # Input: reference to class and a slurpy attr hash sub ::create_opaque_instance ($%) { my ($class, %attrs) = @_; my $instance = bless { 'id' => ++$instance_counter, 'class' => $class, 'attrs' => \%attrs, }, 'Dispatchable'; lock_keys(%{$instance}); return $instance; } # Accessors for the inside of the opaque structure sub ::opaque_instance_id ($) : lvalue { shift->{id} } sub ::opaque_instance_class ($) : lvalue { ${shift->{class}} } sub ::opaque_instance_attrs ($) : lvalue { shift->{attrs} } } { package Dispatchable; use Carp 'confess'; sub isa { our $AUTOLOAD = 'isa'; goto &AUTOLOAD; } sub can { our $AUTOLOAD = 'can'; goto &AUTOLOAD; } sub AUTOLOAD { my $label = (split '::', our $AUTOLOAD)[-1]; return if $label eq 'DESTROY'; my $class = ::opaque_instance_class($_[0]); while (defined $class) { my $method = ::opaque_instance_attrs($class)->{'%:methods'}{$label}; goto &$method if $method; # try again in the superclass $class = $class->superclass; } confess "No method found for $label"; } } # The 'Class' class -- placed here so ::create_class can refer to it my $Class; sub ::create_class (%) { my (%attrs) = @_; return ::create_opaque_instance( # < a Class object is an instance of the Class class > \$Class, ( '$:name' => '', '$:superclass' => undef, '%:attributes' => [], '%:methods' => {}, # and override anything here ... %attrs, ) ); } # The 'Class' class $Class = ::create_class( '$:name' => 'Class', '%:methods' => { 'new' => sub ($%) { my ($class, %attrs) = @_; return ::create_opaque_instance(\$class, %attrs); }, 'name' => sub ($) { ::opaque_instance_attrs(shift)->{'$:name'} }, 'class_precendence_list' => sub ($) { my ($self) = @_; my @cpl = ($self); my $current = $self; while (my $super = $current->superclass) { push @cpl => $super; $current = $super; } return @cpl; }, 'superclass' => sub ($) { ::opaque_instance_attrs(shift)->{'$:superclass'} }, 'get_method' => sub ($$) { my ($self, $label) = @_; ::opaque_instance_attrs($self)->{'%:methods'}->{$label}; }, 'has_method' => sub ($$) { my ($self, $label) = @_; exists ::opaque_instance_attrs($self)->{'%:methods'}->{$label} ? 1 : 0; }, 'add_method' => sub ($$$) { my ($self, $label, $method) = @_; ::opaque_instance_attrs($self)->{'%:methods'}->{$label} = $method; }, 'get_method_list' => sub ($) { my ($self) = @_; keys %{::opaque_instance_attrs($self)->{'%:methods'}}; }, }, ); # The 'Object' class my $Object = $Class->new( '$:name' => 'Object', '%:methods' => { 'id' => sub ($) { ::opaque_instance_id(shift) }, 'class' => sub ($) { ::opaque_instance_class(shift) } }, ); # < Class is a subclass of Object > ::opaque_instance_attrs($Class)->{'$:superclass'} = $Object; ## ---------------------------------------------------------------------------- ## BOOTSTRAPPING Class/Object COMPLETE ## ---------------------------------------------------------------------------- ## ---------------------------------------------------------------------------- ## BOOTSTRAPPING Roles ## ---------------------------------------------------------------------------- my $Role; my $_resolve = sub ($) { my ($self) = @_; my %roles; foreach my $role (@{::opaque_instance_attrs($self)->{'@:roles'}}) { # make a note of the role for does() $roles{::opaque_instance_attrs($role)->{'$:name'}} = undef; foreach my $method_name ($role->get_method_list) { # if we already have the method, then # ignore it and go to the next one next if $self->has_method($method_name); # otherwise ... add the method in $self->add_method($method_name => $role->get_method($method_name)); } } # this is our does() method, it will # allow us to ask if the instance my $_does = sub { exists $roles{$_[1]} ? 1 : 0 }; $self->add_method('does' => $_does); }; # our class(Role), which is also the role(Role) as well $Role = $Class->new( '$:name' => 'Role', '$:superclass' => $Object, '%:methods' => { 'resolve' => $_resolve, 'add_method' => sub ($$$) { my ($self, $label, $method) = @_; ::opaque_instance_attrs($self)->{'%:methods'}->{$label} = $method; }, 'get_method' => sub ($$) { my ($self, $label) = @_; ::opaque_instance_attrs($self)->{'%:methods'}->{$label}; }, 'get_method_list' => sub ($$) { my ($self, $label) = @_; keys %{::opaque_instance_attrs($self)->{'%:methods'}}; }, } ); ok(!$Class->has_method('resolve'), '... we do not have the resolve method'); # Bootstrap -> Class does Role ::opaque_instance_attrs($Class)->{'@:roles'} = [ $Role ]; $_resolve->($Class); # Bootstrap -> Role does Role ::opaque_instance_attrs($Role)->{'@:roles'} = [ $Role ]; $Role->resolve(); # Role can resolve itself now :) # Class gets a special case Does # to avoid endless recursion $Class->add_method('does' => sub { return $_[1] eq 'Role' if $_[0] == $Class; ::opaque_instance_attrs($_[0])->{'%:methods'}->{'does'}->(@_); }); # now Class does Role ## ---------------------------------------------------------------------------- ## BOOTSTRAPPING Roles COMPLETE ## ---------------------------------------------------------------------------- ok($Class->does('Role'), '... Class now does Role'); ok($Class->has_method('resolve'), '... we now have the resolve method'); ok($Role->has_method('does'), '... we now have the does() method'); ok($Role->does('Role'), '... Role now does Role'); ## create some roles my $rFoo = $Role->new( '$:name' => 'rFoo', '%:methods' => { foo => sub { 'rFoo::foo' } } ); ok($rFoo->does('Role'), '... rFoo now does Role'); my $rBar = $Role->new( '$:name' => 'rBar', '%:methods' => { bar => sub { 'rBar::bar' } } ); ok($rBar->does('Role'), '... rBar now does Role'); # create a class to put the roles in ... my $FooBar = $Class->new( '$:name' => 'FooBar', '$:superclass' => $Object, '@:roles' => [ $rFoo, $rBar ], # << tell the class we want these roles '%:methods' => { 'foo_bar' => sub { 'FooBar::foo_bar' } } ); is_deeply( [ $FooBar->get_method_list ], [ 'foo_bar' ], '... got the right method list (unresolved)'); # resolve the class and pull in all the roles $FooBar->resolve(); is_deeply( [ sort $FooBar->get_method_list ], [ 'bar', 'does', 'foo', 'foo_bar' ], '... got the right method list (resolved)'); ok($FooBar->does('rFoo'), '... FooBar does rFoo'); ok($FooBar->does('rBar'), '... FooBar does rBar'); # and our instances do the roles as well my $iFooBar = $FooBar->new(); ok($iFooBar->does('rFoo'), '... iFooBar does rFoo'); ok($iFooBar->does('rBar'), '... iFooBar does rBar'); 1;