package Rose::Object::MixIn; use strict; use Carp; our $Debug = 0; our $VERSION = '0.856'; use Rose::Class::MakeMethods::Set ( inheritable_set => [ '_export_tag' => { list_method => '_export_tags', clear_method => 'clear_export_tags', add_method => '_add_export_tag', delete_method => 'delete_export_tag', deletes_method => 'delete_export_tags', }, '_pre_import_hook', { clear_method => 'clear_pre_import_hooks', add_method => 'add_pre_import_hook', adds_method => 'add_pre_import_hooks', delete_method => 'delete_pre_import_hook', deletes_method => 'delete_pre_import_hooks', }, ], ); sub import { my($class) = shift; my $target_class = (caller)[0]; my($force, @methods, %import_as); foreach my $arg (@_) { if(!defined $target_class && $arg !~ /^-/) { $target_class = $arg; next; } if($arg =~ /^-?-force$/) { $force = 1; } elsif($arg =~ /^-?-target[-_]class$/) { $target_class = undef; # set on next iteration...lame next; } elsif($arg =~ /^:(.+)/) { my $methods = $class->export_tag($1) or croak "Unknown export tag - '$arg'"; push(@methods, @$methods); } elsif(ref $arg eq 'HASH') { while(my($method, $name) = each(%$arg)) { push(@methods, $method); $import_as{$method} = $name; } } else { push(@methods, $arg); } } foreach my $method (@methods) { my $code = $class->can($method) or croak "Could not import method '$method' from $class - no such method"; my $import_as = $import_as{$method} || $method; if($target_class->can($import_as) && !$force) { croak "Could not import method '$import_as' from $class into ", "$target_class - a method by that name already exists. ", "Pass a '-force' argument to import() to override ", "existing methods." } if(my $hooks = $class->pre_import_hooks($method)) { foreach my $code (@$hooks) { my $error; TRY: { local $@; eval { $code->($class, $method, $target_class, $import_as) }; $error = $@; } if($error) { croak "Could not import method '$import_as' from $class into ", "$target_class - $error"; } } } no strict 'refs'; $Debug && warn "${target_class}::$import_as = ${class}->$method\n"; *{$target_class . '::' . $import_as} = $code; } } sub export_tag { my($class, $tag) = (shift, shift); if(index($tag, ':') == 0) { croak 'Tag name arguments to export_tag() should not begin with ":"'; } if(@_ && !$class->_export_tag_value($tag)) { $class->_add_export_tag($tag); } if(@_ && (@_ > 1 || (ref $_[0] || '') ne 'ARRAY')) { croak 'export_tag() expects either a single tag name argument, ', 'or a tag name and a reference to an array of method names'; } my $ret = $class->_export_tag_value($tag, @_); croak "No such tag: $tag" unless($ret); return wantarray ? @$ret : $ret; } sub export_tags { my($class) = shift; return $class->_export_tags unless(@_); $class->clear_export_tags; $class->add_export_tags(@_); } sub add_export_tags { my($class) = shift; while(@_) { my($tag, $arg) = (shift, shift); $class->export_tag($tag, $arg); } } sub pre_import_hook { my($class, $method) = (shift, shift); if(@_ && !$class->_pre_import_hook_value($method)) { $class->add_pre_import_hook($method); } if(@_ && (@_ > 1 || (ref $_[0] && (ref $_[0] || '') !~ /\A(?:ARRAY|CODE)\z/))) { croak 'pre_import_hook() expects either a single method name argument, ', 'or a method name and a code reference or a reference to an array ', 'of code references'; } if(@_) { unless(ref $_[0] eq 'ARRAY') { $_[0] = [ $_[0] ]; } } my $ret = $class->_pre_import_hook_value($method, @_) || []; return wantarray ? @$ret : $ret; } sub pre_import_hooks { shift->pre_import_hook(shift) } 1; __END__ =head1 NAME Rose::Object::MixIn - A base class for mix-ins. =head1 SYNOPSIS package MyMixInClass; use Rose::Object::MixIn(); # Use empty parentheses here our @ISA = qw(Rose::Object::MixIn); __PACKAGE__->export_tag(all => [ qw(my_cool_method my_other_method) ]); sub my_cool_method { ... } sub my_other_method { ... } ... package MyClass; # Import methods my_cool_method() and my_other_method() use MyMixInClass qw(:all); ... package MyOtherClass; # Import just my_cool_method() use MyMixInClass qw(my_cool_method); ... package YetAnotherClass; # Import just my_cool_method() as cool() use MyMixInClass { my_cool_method => 'cool' } =head1 DESCRIPTION L is a base class for mix-ins. A mix-in is a class that exports methods into another class. This export process is controlled with an L-like interface, but L does not inherit from L. When you L a L-derived class, its L method is called at compile time. In other words, this: use Rose::Object::MixIn 'a', 'b', { c => 'd' }; is the same thing as this: BEGIN { Rose::Object::MixIn->import('a', 'b', { c => 'd' }) } To prevent the L method from being run, put empty parentheses "()" after the package name instead of a list of arguments. use Rose::Object::MixIn(); See the L for an example of when this is handy: using L from within a subclass. Note that the empty parenthesis are important. The following is I equivalent: # This is not the same thing as the example above! use Rose::Object::MixIn; See the documentation for the L method below to learn what arguments it accepts. =head1 CLASS METHODS =over 4 =item B Import the methods specified by ARGS into the package from which this method was called. If the current class L already perform one of these methods, a fatal error will occur. To override an existing method, you must use the C<-force> argument (see below). Valid formats for ARGS are as follows: =over 4 =item * B Literal method names will be imported as-is. =item * B Tags names are indicated with a leading colon. For example, ":all" specifies the "all" tag. A tag is a stand-in for a list of methods. See the L method to learn how to create tags. =item * B Each key/value pair in this hash contains a method name and the name that it will be imported as. Use this feature to import methods under different names in order to avoid conflicts with existing methods. =item * C<-force> The special literal argument C<-force> will cause the specified methods to be imported even if the calling class L already perform one or more of those methods. =item * C<-target_class CLASS> The special literal argument C<-target-class> followed by a class name will cause the specified methods to be imported into CLASS rather than into the calling class. =back See the L for several examples of the L method in action. (Remember, it's called implicitly when you L a L-derived class with anything other than an empty set of parenthesis "()" as an argument.) =item B Delete the entire list of L. =item B Get or set the list of method names associated with a tag. The tag name should I begin with a colon. If ARRAYREF is passed, then the list of methods associated with the specific tag is set. Returns a list (in list context) or a reference to an array (in scalar context) of method names. The array reference return value should be treated as read-only. If no such tag exists, and if an ARRAYREF is not passed, then a fatal error will occur. =item B Returns a list (in list context) and a reference to an array (in scalar context) containing the complete list of export tags. The array reference return value should be treated as read-only. =back =head1 AUTHOR John C. Siracusa (siracusa@gmail.com) =head1 LICENSE Copyright (c) 2010 by John C. Siracusa. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.