Skip to content

Commit

Permalink
feat: handle non-positive integer type
Browse files Browse the repository at this point in the history
Non-positive integer can be used as below. It will accept any value
equal to or lower than zero.

```php
final class SomeClass
{
    /** @var non-positive-int */
    public int $nonPositiveInteger;
}
```
  • Loading branch information
romm committed Oct 19, 2023
1 parent f444eae commit 53e4047
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/pages/usage/type-reference.md
Expand Up @@ -22,6 +22,9 @@ final class SomeClass
/** @var negative-int */
private int $negativeInteger,

/** @var non-positive-int */
private int $nonPositiveInteger,

/** @var non-negative-int */
private int $nonNegativeInteger,

Expand Down
2 changes: 2 additions & 0 deletions src/Definition/Repository/Cache/Compiler/TypeCompiler.php
Expand Up @@ -29,6 +29,7 @@
use CuyZ\Valinor\Type\Types\NonEmptyListType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
Expand Down Expand Up @@ -59,6 +60,7 @@ public function compile(Type $type): string
case $type instanceof NativeIntegerType:
case $type instanceof PositiveIntegerType:
case $type instanceof NegativeIntegerType:
case $type instanceof NonPositiveIntegerType:
case $type instanceof NonNegativeIntegerType:
case $type instanceof NativeStringType:
case $type instanceof NonEmptyStringType:
Expand Down
2 changes: 2 additions & 0 deletions src/Type/Parser/Lexer/Token/NativeToken.php
Expand Up @@ -15,6 +15,7 @@
use CuyZ\Valinor\Type\Types\NegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
Expand Down Expand Up @@ -68,6 +69,7 @@ private static function type(string $symbol): ?Type
'float' => NativeFloatType::get(),
'positive-int' => PositiveIntegerType::get(),
'negative-int' => NegativeIntegerType::get(),
'non-positive-int' => NonPositiveIntegerType::get(),
'non-negative-int' => NonNegativeIntegerType::get(),
'string' => NativeStringType::get(),
'non-empty-string' => NonEmptyStringType::get(),
Expand Down
57 changes: 57 additions & 0 deletions src/Type/Types/NonPositiveIntegerType.php
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Type\Types;

use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
use CuyZ\Valinor\Mapper\Tree\Message\MessageBuilder;
use CuyZ\Valinor\Type\IntegerType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Utility\IsSingleton;

/** @internal */
final class NonPositiveIntegerType implements IntegerType
{
use IsSingleton;

public function accepts(mixed $value): bool
{
return is_int($value) && $value <= 0;
}

public function matches(Type $other): bool
{
if ($other instanceof UnionType) {
return $other->isMatchedBy($this);
}

return $other instanceof self
|| $other instanceof NativeIntegerType
|| $other instanceof MixedType;
}

public function canCast(mixed $value): bool
{
return ! is_bool($value)
&& filter_var($value, FILTER_VALIDATE_INT) !== false
&& $value <= 0;
}

public function cast(mixed $value): int
{
assert($this->canCast($value));

return (int)$value; // @phpstan-ignore-line
}

public function errorMessage(): ErrorMessage
{
return MessageBuilder::newError('Value {source_value} is not a valid non-positive integer.')->build();
}

public function toString(): string
{
return 'non-positive-int';
}
}
Expand Up @@ -30,6 +30,7 @@
use CuyZ\Valinor\Type\Types\NonEmptyListType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
Expand Down Expand Up @@ -85,6 +86,7 @@ public function type_is_compiled_correctly_data_provider(): iterable
yield [NativeIntegerType::get()];
yield [PositiveIntegerType::get()];
yield [NegativeIntegerType::get()];
yield [NonPositiveIntegerType::get()];
yield [NonNegativeIntegerType::get()];
yield [new IntegerValueType(1337)];
yield [new IntegerValueType(-1337)];
Expand Down
19 changes: 19 additions & 0 deletions tests/Functional/Type/Parser/Lexer/NativeLexerTest.php
Expand Up @@ -67,6 +67,7 @@
use CuyZ\Valinor\Type\Types\NonEmptyListType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
Expand Down Expand Up @@ -293,6 +294,24 @@ public function parse_valid_types_returns_valid_result_data_provider(): iterable
'type' => NonNegativeIntegerType::class,
];

yield 'Non-positive integer type' => [
'raw' => 'non-positive-int',
'transformed' => 'non-positive-int',
'type' => NonPositiveIntegerType::class,
];

yield 'Non-positive integer type - uppercase' => [
'raw' => 'NON-POSITIVE-INT',
'transformed' => 'non-positive-int',
'type' => NonPositiveIntegerType::class,
];

yield 'Non-positive integer type followed by description' => [
'raw' => 'non-positive-int lorem ipsum',
'transformed' => 'non-positive-int',
'type' => NonPositiveIntegerType::class,
];

yield 'Positive integer value' => [
'raw' => '1337',
'transformed' => '1337',
Expand Down
8 changes: 8 additions & 0 deletions tests/Integration/Mapping/Object/ScalarValuesMappingTest.php
Expand Up @@ -27,6 +27,7 @@ public function test_values_are_mapped_properly(): void
'integer' => 1337,
'positiveInteger' => 1337,
'negativeInteger' => -1337,
'nonPositiveInteger' => -1337,
'nonNegativeInteger' => 1337,
'integerRangeWithPositiveValue' => 1337,
'integerRangeWithNegativeValue' => -1337,
Expand Down Expand Up @@ -62,6 +63,7 @@ public function test_values_are_mapped_properly(): void
self::assertSame(1337, $result->integer);
self::assertSame(1337, $result->positiveInteger);
self::assertSame(-1337, $result->negativeInteger);
self::assertSame(-1337, $result->nonPositiveInteger);
self::assertSame(1337, $result->nonNegativeInteger);
self::assertSame(1337, $result->integerRangeWithPositiveValue);
self::assertSame(-1337, $result->integerRangeWithNegativeValue);
Expand Down Expand Up @@ -117,6 +119,9 @@ class ScalarValues
/** @var negative-int */
public int $negativeInteger = -1;

/** @var non-positive-int */
public int $nonPositiveInteger = -1;

/** @var non-negative-int */
public int $nonNegativeInteger = 1;

Expand Down Expand Up @@ -178,6 +183,7 @@ class ScalarValuesWithConstructor extends ScalarValues
* @param -42.404 $negativeFloatValue
* @param positive-int $positiveInteger
* @param negative-int $negativeInteger
* @param non-positive-int $nonPositiveInteger
* @param non-negative-int $nonNegativeInteger
* @param int<-1337, 1337> $integerRangeWithPositiveValue
* @param int<-1337, 1337> $integerRangeWithNegativeValue
Expand Down Expand Up @@ -205,6 +211,7 @@ public function __construct(
int $integer,
int $positiveInteger,
int $negativeInteger,
int $nonPositiveInteger,
int $nonNegativeInteger,
int $integerRangeWithPositiveValue,
int $integerRangeWithNegativeValue,
Expand Down Expand Up @@ -232,6 +239,7 @@ public function __construct(
$this->integer = $integer;
$this->positiveInteger = $positiveInteger;
$this->negativeInteger = $negativeInteger;
$this->nonPositiveInteger = $nonPositiveInteger;
$this->nonNegativeInteger = $nonNegativeInteger;
$this->integerRangeWithPositiveValue = $integerRangeWithPositiveValue;
$this->integerRangeWithNegativeValue = $integerRangeWithNegativeValue;
Expand Down
149 changes: 149 additions & 0 deletions tests/Unit/Type/Types/NonPositiveIntegerTypeTest.php
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Unit\Type\Types;

use AssertionError;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Tests\Traits\TestIsSingleton;
use CuyZ\Valinor\Type\Types\MixedType;
use CuyZ\Valinor\Type\Types\NativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
use CuyZ\Valinor\Type\Types\UnionType;
use PHPUnit\Framework\TestCase;
use stdClass;

final class NonPositiveIntegerTypeTest extends TestCase
{
use TestIsSingleton;

private NonPositiveIntegerType $nonPositiveIntegerType;

protected function setUp(): void
{
parent::setUp();

$this->nonPositiveIntegerType = new NonPositiveIntegerType();
}

public function test_accepts_correct_values(): void
{
self::assertTrue($this->nonPositiveIntegerType->accepts(0));
self::assertTrue($this->nonPositiveIntegerType->accepts(-404));
}

public function test_does_not_accept_incorrect_values(): void
{
self::assertFalse($this->nonPositiveIntegerType->accepts(null));
self::assertFalse($this->nonPositiveIntegerType->accepts('Schwifty!'));
self::assertFalse($this->nonPositiveIntegerType->accepts(404));
self::assertFalse($this->nonPositiveIntegerType->accepts(42.1337));
self::assertFalse($this->nonPositiveIntegerType->accepts(['foo' => 'bar']));
self::assertFalse($this->nonPositiveIntegerType->accepts(false));
self::assertFalse($this->nonPositiveIntegerType->accepts(new stdClass()));
}

public function test_can_cast_integer_value(): void
{
self::assertTrue($this->nonPositiveIntegerType->canCast(0));
self::assertTrue($this->nonPositiveIntegerType->canCast(-404));
self::assertTrue($this->nonPositiveIntegerType->canCast('-404'));
self::assertTrue($this->nonPositiveIntegerType->canCast(-404.00));
}

public function test_cannot_cast_other_types(): void
{
self::assertFalse($this->nonPositiveIntegerType->canCast(null));
self::assertFalse($this->nonPositiveIntegerType->canCast(42));
self::assertFalse($this->nonPositiveIntegerType->canCast(-42.1337));
self::assertFalse($this->nonPositiveIntegerType->canCast(42.1337));
self::assertFalse($this->nonPositiveIntegerType->canCast(['foo' => 'bar']));
self::assertFalse($this->nonPositiveIntegerType->canCast('Schwifty!'));
self::assertFalse($this->nonPositiveIntegerType->canCast(false));
self::assertFalse($this->nonPositiveIntegerType->canCast(new stdClass()));
}

/**
* @dataProvider cast_value_returns_correct_result_data_provider
*/
public function test_cast_value_returns_correct_result(mixed $value, int $expected): void
{
self::assertSame($expected, $this->nonPositiveIntegerType->cast($value));
}

public function cast_value_returns_correct_result_data_provider(): array
{
return [
'Integer from float' => [
'value' => -404.00,
'expected' => -404,
],
'Integer from string' => [
'value' => '-42',
'expected' => -42,
],
'Integer from integer' => [
'value' => -1337,
'expected' => -1337,
],
'Zero from string' => [
'value' => '0',
'expected' => 0,
],
];
}

public function test_cast_invalid_value_throws_exception(): void
{
$this->expectException(AssertionError::class);

$this->nonPositiveIntegerType->cast('foo');
}

public function test_cast_invalid_positive_value_throws_exception(): void
{
$this->expectException(AssertionError::class);

$this->nonPositiveIntegerType->cast(1337);
}

public function test_string_value_is_correct(): void
{
self::assertSame('non-positive-int', $this->nonPositiveIntegerType->toString());
}

public function test_matches_valid_integer_type(): void
{
self::assertTrue($this->nonPositiveIntegerType->matches(new NativeIntegerType()));
self::assertTrue($this->nonPositiveIntegerType->matches($this->nonPositiveIntegerType));
self::assertFalse($this->nonPositiveIntegerType->matches(new PositiveIntegerType()));
}

public function test_does_not_match_other_type(): void
{
self::assertFalse($this->nonPositiveIntegerType->matches(new FakeType()));
}

public function test_matches_mixed_type(): void
{
self::assertTrue($this->nonPositiveIntegerType->matches(new MixedType()));
}

public function test_matches_union_type_containing_integer_type(): void
{
$union = new UnionType(new FakeType(), new NativeIntegerType(), new FakeType());
$unionWithSelf = new UnionType(new FakeType(), new NonPositiveIntegerType(), new FakeType());

self::assertTrue($this->nonPositiveIntegerType->matches($union));
self::assertTrue($this->nonPositiveIntegerType->matches($unionWithSelf));
}

public function test_does_not_match_union_type_not_containing_integer_type(): void
{
$unionType = new UnionType(new FakeType(), new FakeType());

self::assertFalse($this->nonPositiveIntegerType->matches($unionType));
}
}

0 comments on commit 53e4047

Please sign in to comment.