We use Perl Moo.
Let there is defined a set of attributes:
package C;
use Moo;
use Types::Standard qw(Str Int Num Maybe);
has 'x' => (is=>'rw', isa=>Str);
has 'y' => (is=>'rw', isa=>Int);
has 'z' => (is=>'rw', isa=>Int);
# here to insert make_optional() described below
1;
I want to write a routine which will replace T with Maybe[T] for some attributes. For example: make_optional(qw(x y))
should make type of x
Maybe[Str]
and type of y
Maybe[Int]
.
How to do it with Moo?
[[Note that the Maybe
type doesn't really make an attribute optional per-se, but undef-tolerant. Moo attributes are already optional by default. But for the sake of discussion, I'll continue to use the terminology of optional versus required.]]
Because I don't like "you can't" answers, here's some code that does what you want...
use strict;
use warnings;
BEGIN {
package MooX::MakeOptional;
use Types::Standard qw( Maybe Any );
use Exporter::Shiny our @EXPORT = qw( make_optional has );
use namespace::clean;
sub _exporter_validate_opts {
my $opts = pop;
$opts->{orig_has} = do {
no strict 'refs';
\&{ $opts->{into} . '::has' };
};
$opts->{attributes} = [];
'namespace::clean'->clean_subroutines( $opts->{into}, 'has' );
}
sub _generate_has {
my $opts = pop;
my $attributes = $opts->{attributes};
return sub {
my ( $name, %spec ) = @_;
if ( ref($name) eq 'ARRAY' ) {
push @$attributes, $_, { %spec } for @$name;
}
else {
push @$attributes, $name, \%spec;
}
};
}
sub _generate_make_optional {
my $opts = pop;
my $attributes = $opts->{attributes};
my $orig_has = $opts->{orig_has};
return sub {
my %optional;
$optional{$_} = 1 for @_;
while ( @$attributes ) {
my ( $name, $spec ) = splice( @$attributes, 0, 2 );
if ( $optional{$name} ) {
$spec->{isa} = Maybe[ $spec->{isa} or Any ];
}
$orig_has->( $name, %$spec );
}
}
}
}
{
package C;
use Moo;
use MooX::MakeOptional;
use Types::Standard qw( Str Int );
has 'x' => ( is => 'rw', isa => Str );
has 'y' => ( is => 'rw', isa => Int );
has 'z' => ( is => 'rw', isa => Int );
make_optional( qw(x y) );
}
What this does is replace Moo's has
keyword with a dummy replacement which does nothing except stash the attribute definitions into an array.
Then when make_optional
is called, this runs through the array, and passes each attribute definition to Moo's original has
keyword, but altered to be optional if specified.
Classes that use MooX::MakeOptional
always need to ensure they call the make_optional
function at the end of the class definition, even if they have no optional attributes. If they have no optional attributes, they should just call make_optional
and pass it an empty list.