tomred has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks,

I have been wresting with a problem. I often need to parse and validate (string) data before inserting it into a DB. I've come to rely on Type::Tiny for this kind of validation.

In this case the data cannot be longer than x varchar(x). I often find the data is a empty etring, e.g. ''. If the data is an empty string, I would to rather return undef (null) than ''. In terms of terminology, I am not sure if this is a coercion or not. HTML::FormHandler has the notion of Transform(ations) but I haven't seen a means of appying transformations in the Moo world.

I have become stuck trying to finding a way to allow for a stirng to be either undefined, or if defined less than x or if an emtpy string, undefined.

It could be written a like (untested) this: $data = $_ eq '' ? undef : $_ < $x ? $_ : undef But I'd like to stick to the Types eco system albeit my grasp of it is somethimes failing. Here is my latest effort. The test warning because $_ is not numeric is tricky to side-step.
use v5.22; use warnings; use Test::More; use Test::Deep; use Test::Warnings; use lib qw(lib t); use Type::Tiny; use Types::Standard qw( Maybe Str ); use Types::Common::String qw( NonEmptyStr ); use Type::Params qw( compile_named ); =head2 The goal is to allow a "caption", if defined, should be less than $cap +size. If caption == '', I'd like to return the value as undef. =cut my $capsize = 2781; # This will warn when $_ in not numeric my $LongCaption = Type::Tiny->new( name => 'Caption', constraint => sub { length($_) < $capsize }, message => sub { "Caption needs to be less than $capsize charac +ters" } ); my $EmptyStr = "Type::Tiny"->new( name => 'EmptyString', constraint => sub { $_ eq '' }, message => sub { "An empty string" } ); $LongCaption->plus_coercions( $EmptyStr => sub { warn "Coercing"; return undef; }, ); state $check = Type::Params::compile_named( caption => Maybe[$LongCaption], ); my $text = ''; my $val = $LongCaption->assert_return( $text ); note $val; is($val, undef, 'coerced into undef'); my $data = $check->( { caption => $text } ); cmp_deeply($data, { caption => undef }, 'coerced into undef'); done_testing;

Thanks in advance. I do hope I have not made a silly error.


Dermot

Replies are listed 'Best First'.
Re: Coerce or transform with Types::Param
by 1nickt (Canon) on Mar 19, 2021 at 21:03 UTC

    Hi, I have not been able to solve your coercion problem yet, but you might be interested in Types::Common::String (which comes bundled with Type::Tiny) for working with strings, string lengths, etc.

    Hope this helps!


    The way forward always starts with a minimal test.
      I am familair with and did use StrLength for this code but I was not sure if it inherited from NonEmptyStr which would cause the data to fail validation, StrLenght[1, 2871] might work but if the value meets the constraint, there is no way to coerce it.

      Dermot

        I believe it inherits from SimpleStr which allows zero length.

        FWIW here's my code, which exercises the coercion when the string is empty, but does not seem to apply it. ( Update: added debug and showed output )

        use strict; use warnings; use feature 'say', 'state'; use Types::Standard 'Undef'; use Types::Common::String 'SimpleStr','StrLength'; use Type::Params 'compile'; state $check = compile ( Undef | (SimpleStr & (StrLength[1,4]))->plus_coercions( SimpleStr, sub { warn 'coercing'; length ? $_ : undef }, ) ); #no warnings 'uninitialized'; for my $str ( undef, '', 'abcd', 'abcde' ) { my $label = ! defined($str) ? 'undef' : ! length ($str) ? 'empty' +: $str; my $checked = $check->($str); say sprintf('%s : >%s<', $label, $str); }

        This outputs:

        Use of uninitialized value in sprintf at 11130005.pl line 20. undef : >< coercing at 11130005.pl line 10. empty : >< abcd : >abcd< coercing at 11130005.pl line 10. Value "abcde" did not pass type constraint "Undef|__ANON__" (in $_[0]) + at 11130005.pl line 18 "Undef|__ANON__" requires that the value pass "Undef" or "__ANON__ +" Value "abcde" did not pass type constraint "Undef" Value "abcde" did not pass type constraint "Undef" "Undef" is defined as: (!defined($_)) Value "abcde" did not pass type constraint "__ANON__" is a subtype of "SimpleStr&StrLength[1,4]" "SimpleStr&StrLength[1,4]" requires that the value pass "Simpl +eStr" and "StrLength[1,4]" Value "abcde" did not pass type constraint "StrLength[1,4]" "StrLength[1,4]" expects length($_) to be between 1 and 4 length($_) is 5

        Hope this helps!


        The way forward always starts with a minimal test.
Re: Coerce or transform with Types::Param
by tobyink (Canon) on Mar 21, 2021 at 11:00 UTC

    Something like this?

    use Types::Standard qw( Str Maybe ); use Types::Common::String qw( StrLength ); my $Caption = StrLength[ 1, 2780 ]; my $EmptyStr = Str->where( q{ $_ eq '' } ); my $MaybeCaption = Maybe->of( $Caption )->plus_coercions( $EmptyStr => + q{ undef } ); use Test::More; ok( $MaybeCaption->check( 'My pic' ), 'Good caption passes type constr +aint' ); ok( $MaybeCaption->check( undef ), 'Undef passes type constraint' ); ok( ! $MaybeCaption->check( [] ), 'Arrayref fails type constraint' ); is( $MaybeCaption->coerce( '' ), undef, 'Empty string coerced to undef +' );

    Basically, add the coercion to the Maybe[Caption], not to Caption.

    But the important thing is to make sure that the empty string isn't considered a valid Caption. Because if a value is valid, it is never coerced.

      I was hoping you'd reply Toby, thank you.

      I made a couple of changes to the code so it compiles, and added a test to see that one can pass in the empty string and it will pass validation by being coerced to undef. But the syntax to do this must be wrong:

      ok( $Caption->check( $Caption->coerce('') ), 'Empty string passes' );
      ... I feel sure that there's a way to avoid calling coerce manually.

      Here's the revised test script:

      use Types::Standard qw( Maybe Str ); use Types::Common::String qw( StrLength ); my $NonEmptyCaption = StrLength[ 1, 10 ]; my $EmptyStr = Str->where( q{ $_ eq '' } ); my $Caption = (Maybe[$NonEmptyCaption])->plus_coercions( $EmptyStr => 'undef', ); use Test::More; ok( $Caption->check('My pic'), 'Good caption passes' ); ok( ! $Caption->check('This is too long'), 'Bad caption fails' ); ok( $Caption->check(undef), 'Undef passes' ); ok( ! $Caption->check([]), 'Arrayref fails' ); is( $Caption->coerce(''), undef, 'Empty string coerced to +undef' ); ok( $Caption->check( $Caption->coerce('') ), 'Empty string passes' ); done_testing;
      Outputs:
      perl 11130005-3.pl ok 1 - Good caption passes ok 2 - Bad caption fails ok 3 - Undef passes ok 4 - Arrayref fails ok 5 - Empty string coerced to undef ok 6 - Empty string passes 1..6

      Wonder if there is a more idiomatic way to do the check when a coercion is used. Thanks!


      The way forward always starts with a minimal test.
        ... I feel sure that there's a way to avoid calling coerce manually.

        That was why I used Types::Params because the POD says: ...it will check the arguments passed to it conform to the spec (coercing them if appropriate)

        Otherwise I've had to relie on has_coercions before attempting to call coerce

        If there is another way though, I would love to know about it.

        Dermot
      I too hoped you'd reply.

      Your code is far less verbose than mine too.

      There are a couple of concepts that are new to me there, I've not come across of before and I haven't see a Type modified with where in that way either. I only considered where in the constructor so that is really useful.

      ...
      my $MaybeCaption = Maybe ->of( $LongCaption ) ->plus_coercions( $EmptyStr => q{ undef } );
      Really appreciate the help.
      Dermot

        of is a shorthand for parameterizing a type. If you do something like this, it will fail:

        if ( ArrayRef[Int]->check( \@numbers ) ) { ...; }

        And you need to add some parentheses which look pretty ugly:

        if ( ( ArrayRef[Int] )->check( \@numbers ) ) { ...; }

        The of method look a little nicer:

        if ( ArrayRef->of( Int )->check( \@numbers ) ) { ...; }

        And where is a shorthand for creating a child type with an additional restriction.