Search code examples
perlmoose

Apply specific check (beyond Moose types) to Moose attribute


Moose types are great, but sometimes you need to be more specific. You all know these data type rules: that parameter may only be 'A', 'B' or 'C', or only a currency symbol, or must conform to some regular expression.

Take a look at the following example which has two constrained attributes, one must be either 'm' or 'f', the other must be a valid ISO date. What's the best way in Moose to specify these constraints? I'd think of the SQL CHECK clause, but AFAICS there is no check keyword in Moose. So I used trigger, but it sounds wrong. Anyone has a better answer?

package Person;
use Moose;

has gender          => is => 'rw', isa => 'Str', trigger =>
    sub { confess 'either m or f' if $_[1] !~ m/^m|f$/ };
has name            => is => 'rw', isa => 'Str';
has dateOfBirth     => is => 'rw', isa => 'Str', trigger =>
    sub { confess 'not an ISO date' if $_[1] !~ m/^\d\d\d\d-\d\d-\d\d$/ };

no Moose;
__PACKAGE__->meta->make_immutable;

package main;
use Test::More;
use Test::Exception;

dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';

done_testing;

Here's what I wound up using:

package Blabla::Customer;
use Moose::Util::TypeConstraints;
use Moose;

subtype ISODate => as 'Str' => where { /^\d\d\d\d-\d\d-\d\d$/ };

has id              => is => 'rw', isa => 'Str';
has gender          => is => 'rw', isa => enum ['m', 'f'];
has firstname       => is => 'rw', isa => 'Str';
has dateOfBirth     => is => 'rw', isa => 'ISODate';

no Moose;
__PACKAGE__->meta->make_immutable;

This is Moose version 1.19, in case it matters. I got the following warning for the wrong subtype as => 'Str', where => { ... } syntax I erroneously introduced: Calling subtype() with a simple list of parameters is deprecated. So I had to change it a bit according to the fine manual.


Solution

  • Just define your own subtype, and use that.

    package Person;
    
    use Moose::Util::TypeConstraints;
    
    use namespace::clean;
    use Moose;
    
    has gender => (
      is => 'rw',
      isa => subtype(
        as 'Str',
        where { /^[mf]$/ }
      ),
    );
    has name => (
      is => 'rw',
      isa => 'Str'
    );
    has dateOfBirth => (
      is => 'rw',
      isa => subtype(
        as 'Str',
        where { /^\d\d\d\d-\d\d-\d\d$/ }
      ),
    );
    
    no Moose;
    __PACKAGE__->meta->make_immutable;
    1;
    
    package main;
    use Test::More;
    use Test::Exception;
    
    dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
    dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';
    
    done_testing;
    

    Or you could use the MooseX::Types module.

    package Person::TypeConstraints;
    
    use MooseX::Types::Moose qw'Str';
    use MooseX::Types -declare => [qw'
      Gender ISODate
    '];
    
    subtype Gender, (
      as Str,
      where { /^[mf]$/ },
    );
    
    subtype ISODate, (
      as Str,
      where { /^\d\d\d\d-\d\d-\d\d$/ }
    );
    1;
    
    package Person:
    
    use MooseX::Types::Moose qw'Str';
    use Person::TypeConstraints qw'Gender ISODate';
    
    use namespace::clean;
    use Moose;
    
    has gender => (
      is => 'rw',
      isa => Gender,
    );
    has name => (
      is => 'rw',
      isa => Str,
    );
    has dateOfBirth => (
      is => 'rw',
      isa => ISODate,
    );
    
    no Moose;
    __PACKAGE__->meta->make_immutable;
    1;