tobyink/p5-type-tiny

Allow replacing Error::TypeTiny::Assertion

Ovid opened this issue · 6 comments

Ovid commented

In lib/Type/Tiny.pm, we have this (some of it truncated to show structure):

sub _failed_check {
    require Error::TypeTiny::Assertion;

    my ( $self, $name, $value, %attrs ) = @_;
    $self = $ALL_TYPES{$self} if defined $self && !ref $self;

    my $exception_class =
        delete( $attrs{exception_class} ) || "Error::TypeTiny::Assertion";
    my $callback = delete( $attrs{on_die} );

    if ( $self ) {
        return $exception_class->throw_cb( ... );
        );
    }
    else {
        return $exception_class->throw_cb( ... );
    }
} #/ sub _failed_check

However, every place that calls this does so like this: $self->_failed_check( "$self", $_ );. So that attributes are not passed to _failed_check and we can't replace the exception class.

(Of course, exception_class isn't documented, so this is for some future code?)

A place you theoretically could use it would be subclassing Type::Params::Signature to override _make_constraint_fail.

Under what circumstances were you hoping to override the class? On a per-type-constraint basis? In a particular lexical scope?

Ovid commented

We have a very weird edge case where we're doing things like this with Moo(se):

has some_value => (
    is       => 'ro',
    isa      => SomeConstraint,
    required => 1,
);

This is in some OpenAPI code. It would be nice if we could have SomeConstraint, for this attribute, throw an HTTP::Throwable exception instead of Error::TypeTiny::Assertion. With that, our code magically just works. Otherwise, we've done this:

around 'BUILDARGS' => sub {
    my ( $orig, $class, @args ) = @_;
    my $arg_for = $class->$orig(@args);
    unless ( SomeConstraint->check($arg_for->{some_value}) ) {
        ... throw the exception we want, not the exception we'd get
    }
    ...

So we have to write a bunch of fragile boilerplate when all we want is a different exception.

Ovid commented

Oh, and many of these are in-house constraints, not just default ones shipped with Type::Tiny. And we'd need different exceptions for different types of constraints. If I were to coerce an order from a an order id, it would be nice to have an invalid order id throw a 500, but if we don't find an order for that id, have it throw a 404.

I'd suggest blessing SomeConstraint into a subclass of Type::Tiny and overriding _failed_check, but _failed_check is usually called as a function rather than a method. (Because that way it will still work if the original type constraint has gone out of scope. Yes, that can happen.) So that's not really an option.

I guess the best option for your use case would be for _failed_check to do something like:

    my $exception_class = delete( $attrs{exception_class} )
      || $self->{exception_class}
      || "Error::TypeTiny::Assertion";

With the above patch, something like this should work:

isa => HashRef->of( Num )->create_child_type( exception_class => 'My::Exception::Class' ),

This is included in Type::Tiny 2.003_000 on CPAN. I should release a stable version some time this month.