6

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.

Lumi
  • 14,775
  • 8
  • 59
  • 92

3 Answers3

6

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;
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • 3
    I have to say the reliance upon the 'Str' type when Moose has an Enum TypeConstraint is confusing to me. What is wrong with `has gender => ( isa => enum([ qw|M m F f| ] ), is => 'ro');`? – perigrin Apr 28 '11 at 20:11
  • I wound up using `Moose::Util::TypeConstraints`, which has the advantage of already being installed on the target system (which I do not control). I'm sure the other suggestions are interesting as well. Check my initial post where I'm going to post an update as the syntax for calling `subtype` differs a little bit from the one presented in your example. There's also `enum` which I found comes in handy. Thanks a ton! – Lumi Apr 28 '11 at 20:11
  • I have to make a correction. Brad's syntax for `subtype` was just fine. I spilt the beans by writing `subtype as => 'Str', where => { ... }`, which is wrong, or deprecated. It's a bit tricky this one wants to be spelt out differently than the other attribute modifiers. – Lumi Apr 28 '11 at 20:22
  • @Lumi `where {/./}` is the same as `where sub{/./}` which returns `{where=>sub{/./}}` – Brad Gilbert Jun 08 '11 at 03:46
  • I botched the first part: `as => 'Str'` (wrong) instead of `as 'Str'` (right). – Lumi Jun 08 '11 at 18:08
3

Adding your own type like Brad said:

use Moose::Util::TypeConstraints;

my $gender_constraint = subtype as 'Str', where { $_ =~ /^[FfMm]$/ };
has gender => ( is => 'rw', isa => $gender_constraint );
SymKat
  • 841
  • 5
  • 5
  • That's one way to do it, but it really only makes sense if you're going to use it more than once, and only in one file. I would recommend using [MooseX::Types](http://search.cpan.org/perldoc/MooseX::Types) if you plan on using the same constraints over more than one file. – Brad Gilbert Apr 28 '11 at 18:13
  • Thanks. Although Brad does not seem to agree 100 %, I think it's fine to store and reuse the return value of `subtype`, even though it might be confined to that one scope. Maybe that's what you intend? – Lumi Apr 28 '11 at 20:19
  • @Michael-Ludwig Yes, I often have things like $abs_int and such that are used at multiple places in one class, so I find it works well for that. Brad is right that MooseX::Types is awesome if you're using the same constraints in multiple files, and subtyping inline is good for a single constraint. – SymKat Apr 28 '11 at 21:55
0

You could try using MooseX-Types-Parameterizable to implement types that take parameters for the cases you present (untested, just sketched):

package YourTypes;
use MooseX::Types -declare => [qw( OneOfStr MatchingStr )];
use MooseX::Types::Moose qw( Str ArrayRef RegexpRef );

subtype OneOfStr,
     as Parameterizable[ Str, ArrayRef[ Str ] ],
     where {
         my ($str, $allowed) = @_;
         return scalar grep { $_ eq $str } @$allowed;
     };

subtype MatchingStr,
     as Parameterizable[ Str, RegexpRef ],
     where {
         my ($str, $rx) = @_;
         return scalar $str =~ $rx;
     };

1;

and you would use it like this:

use YourTypes qw( OneOfStr MatchingStr );

has gender => (is => 'ro', isa => OneOfStr[ [qw( f m )] ]);
has dob    => (is => 'ro', isa => MatchingStr[ qr/^$yourregex$/ ]);
phaylon
  • 1,914
  • 14
  • 13