2013 twenty-four merry days of Perl Feed

More Moose, More Discipline

MooseX::StrictConstructor - 2013-12-19

One of the things that makes Moose so convenient is that it makes it easy to validate data without much effort. Perl code tends to be extremely permissive. That is: it's quite common not to find much type checking code, so if bad data is passed in, it's not caught until it's used, at which point the error message can be pretty weird. I know I have seen "not an ARRAY reference" from weird places more times than I'd like to remember.

The reason, at least in part, is simple: type checking is a pain.

Moose makes it easy by associating type data with attributes. When your class includes:

package Thing;
# ⋮
has weight => (
  is => 'ro',
  isa => PositiveInt,
  required => 1,
);

...then you can feel confident at any point in your program that an object's weight is a positive integer. Either the constructor will have thrown a clear exception when the object was constructed or it got the right kind of data.

Types can even be re-used outside the context of attribute definition to keep making data validation really pervasive.

sub eat_pies {
  my ($self, $how_many) = @_;
  PositiveInt->assert_valid($how_many);
# ⋮
}

Except...

Unfortunately, Moose has a gaping hole into which bad data can fall: the constructor.

package Gift {
  use Moose;
  use Moose::Util::TypeConstraints;

  has wrapping_paper => (
    is => 'ro',
    isa => enum([ qw( festive sombre amazon.com ) ]),
    default => 'amazon.com',
  );

# ⋮
}

So, we've got code representing the gift we're carefully picking out and painstakingly wrapping, but we stupidly do this:

my $gift = Gift->new(..., wrappingpaper => 'sombre');

Not only do we get no error, but our friend gets a package wrapped in thoughtless-seeming Amazon wrapping paper. Argh!

Only one change is needed:

package Gift {
  use Moose;
  use Moose::Util::TypeConstraints;
  use MooseX::StrictConstructor; # ← just add this!

  has wrapping_paper => (
    is => 'ro',
    isa => enum([ qw( festive sombre amazon.com ) ]),
    default => 'amazon.com',
  );

# ⋮
}

MooseX::StrictConstructor causes the constructor to throw an exception on unknown input. For example, our bogus wrappingpaper argument, above, would get us the exception:

  Found unknown attribute(s) init_arg passed to the constructor: wrappingpaper

Except...

Sometimes, it's useful to accept an unknown set of extra arguments to your constructor. Maybe you do something like this:

package Gift {
# ⋮

  has shipping_cost => (
    is => 'rw',
    isa => Money,
    default => sub { ...something with size and weight... },
    init_arg => undef,
  );

  sub BUILD {
    my ($self, $arg) = @_;
    $self->carrier('UPS'), $self->shipping_cost(0) if $arg->{free_shipping};
  }
}

Now you can pass free_shipping => 1 to your constructor to clear out the otherwise-computed shipping cost… but it won't work with MooseX::StrictConstructor, because free_shipping isn't the init_arg of any attribute. Fortunately, there's a trivial fix. If you want to accept an argument for use in BUILD just delete it so that it's not there anymore when BUILD is done:

sub BUILD {
  my ($self, $arg) = @_;
  $self->carrier('UPS'), $self->shipping_cost(0) if delete $arg->{free_shipping};
# ^- no more exception!
}

See Also

Gravatar Image This article contributed by: Ricardo Signes <rjbs@cpan.org>