Search code examples
perlmoose

Moose how to change the attribute value only when it is $undef?


Now have:

has 'id' => (
    is => 'rw',
    isa => 'Str',
    default => sub { "id" . int(rand(1000))+1 }
);

Works OK, the:

PKG->new(id => 'some'); #the id is "some"
PKG->new()              #the id is #id<random_number>

In the next scenario:

my $value = undef;
PKG->new(id => $value);

(of course) got an error:

Attribute (id) does not pass the type constraint because: Validation failed for 'Str' with value undef at /Users/me/perl5/perlbrew/perls/perl-5.16.3/lib/site_perl/5.16.3/darwin-thread-multi-2level/Moose/Exception.pm line 37

The question is:

How to achieve changing the value after it is set to undef (and only when it is $undef)? So,

has 'id' => (
    is => 'rw',
    isa => 'Str|Undef',  #added undef to acceptable Type
    default => sub { "id" . int(rand(1000))+1 }
);

Now, it accepting the $undef, but I don't want $undef but want "id" . int(rand(1000))+1. How to change the attribute value after it is set?

The after is called only for the accessors not for constructors. Maybe some weird coercion from Undef to Str - but only for this one attribute?

Ps: using the PKG->new( id => $value // int(rand(10000)) ) is not an acceptable solution. The module should accept the $undef and should silently change it to the random number.


Solution

  • Type::Tiny has as one of its aims to make it easy to add coercions to individual attributes really easy. Here's an example:

    use strict;
    use warnings;
    
    {
        package Local::Test;
        use Moose;
        use Types::Standard qw( Str Undef );
    
        my $_id_default = sub { "id" . int(rand(1000)+1) };
    
        has id => (
            is      => 'rw',
            isa     => Str->plus_coercions(Undef, $_id_default),
            default => $_id_default,
            coerce  => 1,
        );
    
        __PACKAGE__->meta->make_immutable;
    }
    
    print Local::Test->new(id => 'xyz123')->dump;
    print Local::Test->new(id => undef)->dump;
    print Local::Test->new->dump;
    

    You could also look at MooseX::UndefTolerant which makes undef values passed to the constructor act as if they were entirely omitted. This won't cover passing undef to accessors though; just constructors.