perltypesattributesmoo

How to change attribute types? (Perl Moo)


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?


Solution

  • [[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.