From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

=pod
=encoding utf-8
=head1 PURPOSE
If a coercion exists for type C<Foo>, then Type::Tiny should be able to
auto-generate a coercion for type C<< ArrayRef[Foo] >>, etc.
=head1 AUTHOR
Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
=head1 COPYRIGHT AND LICENCE
This software is copyright (c) 2013-2014, 2017-2025 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
use strict;
use lib qw( ./lib ./t/lib ../inc ./inc );
use Types::Standard qw( -types slurpy );
ok(
! Dict->of(x => Int)->has_coercion,
"Dict of type without coercion shouldn't have coercion",
);
ok(
Dict->of(x => Int->plus_coercions(Any, 1))->has_coercion,
"Dict of type with coercion should have coercion",
);
ok(
! Tuple->of(Int)->has_coercion,
"Tuple of type without coercion shouldn't have coercion",
);
ok(
Tuple->of(Int->plus_coercions(Any, 1))->has_coercion,
"Tuple of type with coercion should have coercion",
);
ok(
! Map->of(Str, Int)->has_coercion,
"Map of type without coercion shouldn't have coercion",
);
ok(
Map->of(Str, Int->plus_coercions(Any, 1))->has_coercion,
"Map of type with coercion should have coercion",
);
NONINLINED: {
my $Foo = declare Foo => as Int;
coerce $Foo, from Num, via { int($_) };
my $ArrayOfFoo = declare ArrayOfFoo => as ArrayRef[$Foo], coercion => 1;
ok($ArrayOfFoo->has_coercion, '$ArrayOfFoo has coercion');
my $arr1 = [1..3];
my $arr2 = [1..3, "Hello"];
is(
$ArrayOfFoo->coerce($arr1),
$arr1,
'$ArrayOfFoo does not coerce value that needs no coercion',
);
is_deeply(
$ArrayOfFoo->coerce([1.1, 2.1, 3.1]),
[1, 2, 3],
'$ArrayOfFoo does coerce value that can be coerced',
);
is(
$ArrayOfFoo->coerce($arr2),
$arr2,
'$ArrayOfFoo does not coerce value that cannot be coerced',
);
my $HashOfFoo = HashRef[$Foo];
ok($HashOfFoo->has_coercion, '$HashOfFoo has coercion');
my $hsh1 = {one => 1, two => 2, three => 3};
my $hsh2 = {one => 1, two => 2, three => 3, greeting => "Hello"};
is(
$HashOfFoo->coerce($hsh1),
$hsh1,
'$HashOfFoo does not coerce value that needs no coercion',
);
is_deeply(
$HashOfFoo->coerce({one => 1.1, two => 2.2, three => 3.3}),
{one => 1, two => 2, three => 3},
'$HashOfFoo does coerce value that can be coerced',
);
is(
$HashOfFoo->coerce($hsh2),
$hsh2,
'$HashOfFoo does not coerce value that cannot be coerced',
);
my $RefOfFoo = ScalarRef[$Foo];
ok($RefOfFoo->has_coercion, '$RefOfFoo has coercion');
my $ref1 = do { my $x = 1; \$x };
my $ref2 = do { my $x = "xxx"; \$x };
is(
$RefOfFoo->coerce($ref1),
$ref1,
'$RefOfFoo does not coerce value that needs no coercion',
);
is_deeply(
${ $RefOfFoo->coerce(do { my $x = 1.1; \$x }) },
1,
'$RefOfFoo does coerce value that can be coerced',
);
is(
$RefOfFoo->coerce($ref2),
$ref2,
'$RefOfFoo does not coerce value that cannot be coerced',
);
# This added coercion should be ignored, because undef shouldn't
# need coercion!
my $MaybeFoo = Maybe[$Foo->plus_coercions(Undef, 999)];
is(
$MaybeFoo->coerce(undef),
undef,
'$MaybeFoo does not coerce undef',
);
is(
$MaybeFoo->coerce(42),
42,
'$MaybeFoo does not coerce integer',
);
is(
$MaybeFoo->coerce(4.2),
4,
'$MaybeFoo does coerce non-integer number',
);
is(
$MaybeFoo->coerce("xyz"),
"xyz",
'$MaybeFoo cannot coerce non-number',
);
};
INLINED: {
my $Bar = declare Bar => as Int;
coerce $Bar, from Num, q { int($_) };
$Bar->coercion->freeze;
my $ArrayOfBar = ArrayRef[$Bar];
$ArrayOfBar->coercion->freeze;
ok($ArrayOfBar->has_coercion, '$ArrayOfBar has coercion');
ok($ArrayOfBar->coercion->can_be_inlined, '$ArrayOfBar coercion can be inlined');
my $arr1 = [1..3];
my $arr2 = [1..3, "Hello"];
is(
$ArrayOfBar->coerce($arr1),
$arr1,
'$ArrayOfBar does not coerce value that needs no coercion',
);
is_deeply(
$ArrayOfBar->coerce([1.1, 2.1, 3.1]),
[1, 2, 3],
'$ArrayOfBar does coerce value that can be coerced',
);
is(
$ArrayOfBar->coerce($arr2),
$arr2,
'$ArrayOfBar does not coerce value that cannot be coerced',
);
my $HashOfBar = HashRef[$Bar];
$HashOfBar->coercion->freeze;
ok($HashOfBar->has_coercion, '$HashOfBar has coercion');
ok($HashOfBar->coercion->can_be_inlined, '$HashOfBar coercion can be inlined');
my $hsh1 = {one => 1, two => 2, three => 3};
my $hsh2 = {one => 1, two => 2, three => 3, greeting => "Hello"};
is(
$HashOfBar->coerce($hsh1),
$hsh1,
'$HashOfBar does not coerce value that needs no coercion',
);
is_deeply(
$HashOfBar->coerce({one => 1.1, two => 2.2, three => 3.3}),
{one => 1, two => 2, three => 3},
'$HashOfBar does coerce value that can be coerced',
);
is(
$HashOfBar->coerce($hsh2),
$hsh2,
'$HashOfBar does not coerce value that cannot be coerced',
);
my $RefOfBar = ScalarRef[$Bar];
$RefOfBar->coercion->freeze;
ok($RefOfBar->has_coercion, '$RefOfBar has coercion');
ok($RefOfBar->coercion->can_be_inlined, '$RefOfBar coercion can be inlined');
my $ref1 = do { my $x = 1; \$x };
my $ref2 = do { my $x = "xxx"; \$x };
is(
$RefOfBar->coerce($ref1),
$ref1,
'$RefOfBar does not coerce value that needs no coercion',
);
is_deeply(
${ $RefOfBar->coerce(do { my $x = 1.1; \$x }) },
1,
'$RefOfBar does coerce value that can be coerced',
);
is(
$RefOfBar->coerce($ref2),
$ref2,
'$RefOfBar does not coerce value that cannot be coerced',
);
# This added coercion should be ignored, because undef shouldn't
# need coercion!
my $MaybeBar = Maybe[$Bar->plus_coercions(Undef, 999)];
$MaybeBar->coercion->freeze;
is(
$MaybeBar->coerce(undef),
undef,
'$MaybeBar does not coerce undef',
);
is(
$MaybeBar->coerce(42),
42,
'$MaybeBar does not coerce integer',
);
is(
$MaybeBar->coerce(4.2),
4,
'$MaybeBar does coerce non-integer number',
);
is(
$MaybeBar->coerce("xyz"),
"xyz",
'$MaybeBar cannot coerce non-number',
);
};
MAP: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, q{ length($_) };
my $IntFromNum = declare IntFromNum => as Int;
coerce $IntFromNum, from Num, q{ int($_) };
my $IntFromArray = declare IntFromArray => as Int;
coerce $IntFromArray, from ArrayRef, via { scalar(@$_) };
$_->coercion->freeze for $IntFromStr, $IntFromNum, $IntFromArray;
my $Map1 = Map[$IntFromNum, $IntFromStr];
ok(
$Map1->has_coercion && $Map1->coercion->can_be_inlined,
"$Map1 has an inlinable coercion",
);
is_deeply(
$Map1->coerce({ 1.1 => "Hello", 2.1 => "World", 3.1 => "Hiya" }),
{ 1 => 5, 2 => 5, 3 => 4 },
"Coercions to $Map1",
);
is_deeply(
$Map1->coerce({ 1.1 => "Hello", 2.1 => "World", 3.1 => [] }),
{ 1.1 => "Hello", 2.1 => "World", 3.1 => [] },
"Impossible coercion to $Map1",
);
my $m = { 1 => 2 };
is(
$Map1->coerce($m),
$m,
"Unneeded coercion to $Map1",
);
my $Map2 = Map[$IntFromNum, $IntFromArray];
ok(
$Map2->has_coercion && !$Map2->coercion->can_be_inlined,
"$Map2 has a coercion, but it cannot be inlined",
);
is_deeply(
$Map2->coerce({ 1.1 => [1], 2.1 => [1,2], 3.1 => [] }),
{ 1 => 1, 2 => 2, 3 => 0 },
"Coercions to $Map2",
);
is_deeply(
$Map2->coerce({ 1.1 => [1], 2.1 => [1,2], 3.1 => {} }),
{ 1.1 => [1], 2.1 => [1,2], 3.1 => {} },
"Impossible coercion to $Map2",
);
$m = { 1 => 2 };
is(
$Map2->coerce($m),
$m,
"Unneeded coercion to $Map2",
);
};
DICT: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, q{ length($_) };
my $IntFromNum = declare IntFromNum => as Int;
coerce $IntFromNum, from Num, q{ int($_) };
my $IntFromArray = declare IntFromArray => as Int;
coerce $IntFromArray, from ArrayRef, via { scalar(@$_) };
$_->coercion->freeze for $IntFromStr, $IntFromNum, $IntFromArray;
my @a = (a => $IntFromStr, b => $IntFromNum, c => Optional[$IntFromNum]);
my $Dict1 = Dict[ a => $IntFromStr, b => $IntFromNum, c => Optional[$IntFromNum] ];
ok(
$Dict1->has_coercion && $Dict1->coercion->can_be_inlined,
"$Dict1 has an inlinable coercion",
);
is_deeply(
$Dict1->coerce({ a => "Hello", b => 1.1, c => 2.2 }),
{ a => 5, b => 1, c => 2 },
"Coercion (A) to $Dict1",
);
is_deeply(
$Dict1->coerce({ a => "Hello", b => 1 }),
{ a => 5, b => 1 },
"Coercion (B) to $Dict1",
);
is_deeply(
$Dict1->coerce({ a => "Hello", b => 1, c => [], d => 1 }),
{ a => "Hello", b => 1, c => [], d => 1 },
"Coercion (C) to $Dict1 - changed in 0.003_11; the presence of an additional value cancels coercion",
);
};
DICT_PLUS_SLURPY: {
my $Rounded1 = Int->plus_coercions(Num, q[int($_)]);
my $Dict1 = Dict[ a => $Rounded1, slurpy Map[$Rounded1, $Rounded1] ];
is_deeply(
$Dict1->coerce({ a => 1.1, 2.2 => 3.3, 4.4 => 5 }),
{ a => 1, 2 => 3, 4 => 5 },
"Coercion to $Dict1 (inlined)",
);
my $Rounded2 = Int->plus_coercions(Num, sub { int($_) });
my $Dict2 = Dict[ a => $Rounded2, slurpy Map[$Rounded2, $Rounded2] ];
is_deeply(
$Dict2->coerce({ a => 1.1, 2.2 => 3.3, 4.4 => 5 }),
{ a => 1, 2 => 3, 4 => 5 },
"Coercion to $Dict2 (non-inlined)",
);
};
DICT_PLUS_OPTIONAL: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, sub { length($_) };
$IntFromStr->coercion->freeze;
my $Dict1 = Dict[ a => $IntFromStr, b => Optional[Int], c => Optional[Int] ];
ok(
$Dict1->has_coercion && !$Dict1->coercion->can_be_inlined,
"$Dict1 has a non-inlinable coercion",
);
is_deeply(
$Dict1->coerce({ a => "Hello", b => 1, c => 2 }),
{ a => 5, b => 1, c => 2 },
"Coercion (A) to $Dict1",
);
is_deeply(
$Dict1->coerce({ a => "Hello", b => 1 }),
{ a => 5, b => 1 },
"Coercion (B) to $Dict1",
);
};
TUPLE: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, q{ length($_) };
my $IntFromNum = declare IntFromNum => as Int;
coerce $IntFromNum, from Num, q{ int($_) };
my $IntFromArray = declare IntFromArray => as Int;
coerce $IntFromArray, from ArrayRef, via { scalar(@$_) };
$_->coercion->freeze for $IntFromStr, $IntFromNum, $IntFromArray;
my $Tuple1 = Tuple[ $IntFromNum, Optional[$IntFromStr], slurpy ArrayRef[$IntFromNum]];
ok(
$Tuple1->has_coercion && $Tuple1->coercion->can_be_inlined,
"$Tuple1 has an inlinable coercion",
);
is_deeply(
$Tuple1->coerce([qw( 1.1 1.1 )]),
[1, 3],
"Coercion (A) to $Tuple1",
);
is_deeply(
$Tuple1->coerce([qw( 1.1 1.1 2.2 2.2 33 3.3 )]),
[1, 3, 2, 2, 33, 3],
"Coercion (B) to $Tuple1",
);
my $Tuple2 = Tuple[ $IntFromNum ];
is_deeply(
$Tuple2->coerce([qw( 1.1 )]),
[ 1 ],
"Coercion (A) to $Tuple2",
);
is_deeply(
$Tuple2->coerce([qw( 1.1 2.2 )]),
[ 1.1, 2.2 ],
"Coercion (B) to $Tuple2 - changed in 0.003_11; the presence of an additional value cancels coercion",
);
my $EvenInt = Int->create_child_type(
name => 'EvenInt',
constraint => sub { not $_ % 2 },
);
my $Tuple3 = Tuple[ $EvenInt->plus_coercions(Int, sub { 2 * $_ }) ];
ok( $Tuple3->check([4]) );
ok( not $Tuple3->check([3]) );
is_deeply(
$Tuple3->coerce([4]),
[4],
"No coercion necessary to $Tuple3",
);
is_deeply(
$Tuple3->coerce([3]),
[6],
"Coercion to $Tuple3",
);
my $EvenInt2 = Int->create_child_type(
name => 'EvenInt2',
constraint => sub { not $_ % 2 },
inlined => sub { undef, "not($_ % 2)" },
);
my $Tuple4 = Tuple[ $EvenInt2->plus_coercions(Int, q{ 2 * $_ }) ];
ok( $Tuple4->check([4]) );
ok( not $Tuple4->check([3]) );
is_deeply(
$Tuple4->coerce([4]),
[4],
"No coercion necessary to $Tuple4",
);
is_deeply(
$Tuple4->coerce([3]),
[6],
"Coercion to $Tuple4",
);
};
TUPLE: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, q{ length($_) };
is_deeply(
Tuple->of(HashRef, slurpy ArrayRef[$IntFromStr])->coerce([{}, 1, 2.2, "Hello", "world"]),
[{}, 1, 3, 5, 5],
'coercing Tuple with slurpy arrayref'
);
};
THINGY1: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, q{ length($_) };
is_deeply(
Tuple->of($IntFromStr)->coerce(["Hello","world"]),
["Hello","world"],
'inlinable coercion of Tuple with no slurpy given input with extra fields fails'
);
};
THINGY2: {
my $IntFromStr = declare IntFromStr => as Int;
coerce $IntFromStr, from Str, sub{ length($_) };
is_deeply(
Tuple->of($IntFromStr)->coerce(["Hello","world"]),
["Hello","world"],
'non-inlinable coercion of Tuple with no slurpy given input with extra fields fails'
);
};
THINGY3: {
my $IntFromStr = Int->plus_coercions( Str, 'length($_)' );
my $Tuple = Dict->of( xyz => $IntFromStr, slurpy HashRef[Int] );
is_deeply(
$Tuple->coerce( { xyz => "Foo", abc => 4 } ),
{ xyz => 3, abc => 4 },
'Dict where key has inlineable coercion but slurpy has no coercion'
);
is_deeply(
$Tuple->coerce( { xyz => "Foo", abc => 4.1 } ),
{ xyz => "Foo", abc => 4.1 },
'... all or nothing'
);
}
THINGY4: {
my $IntFromStr = Int->plus_coercions( Str, sub { length($_) } );
my $Tuple = Dict->of( xyz => $IntFromStr, slurpy HashRef[Int] );
is_deeply(
$Tuple->coerce( { xyz => "Foo", abc => 4 } ),
{ xyz => 3, abc => 4 },
'Dict where key has non-inlineable coercion but slurpy has no coercion'
);
is_deeply(
$Tuple->coerce( { xyz => "Foo", abc => 4.1 } ),
{ xyz => "Foo", abc => 4.1 },
'... all or nothing'
);
}
done_testing;