Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"echo \"Decimal\" && ./vendor/bin/pest tests/Unit/Decimal --mutate --covered-only --parallel --min=100",
"echo \"Float\" && ./vendor/bin/pest tests/Unit/Float --mutate --covered-only --parallel --min=100",
"echo \"Base\" && ./vendor/bin/pest tests/Unit/Base --mutate --covered-only --parallel --min=100",
"echo \"Array\" && ./vendor/bin/pest tests/Unit/Array --mutate --covered-only --parallel --min=100",
"echo \"Array\" && ./vendor/bin/pest tests/Unit/ArrayType --mutate --covered-only --parallel --min=100",
"echo \"Bool\" && ./vendor/bin/pest tests/Unit/Bool --mutate --covered-only --parallel --min=100",
"echo \"Undefined\" && ./vendor/bin/pest tests/Unit/Undefined --mutate --covered-only --parallel --min=100"
]
Expand Down
4 changes: 4 additions & 0 deletions src/ArrayType/ArrayEmpty.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
/**
* Immutable empty collection.
*
* Example
* - $v = new ArrayEmpty([]);
* $v->toArray(); // []
*
* @extends ArrayTypeAbstract<never>
*
* @psalm-immutable
Expand Down
4 changes: 4 additions & 0 deletions src/ArrayType/ArrayNonEmpty.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
/**
* Immutable non-empty array.
*
* Example
* - $v = ArrayNonEmpty::fromArray([new \stdClass()]);
* $v->count(); // 1
*
* @template TItem of object
*
* @template-extends ArrayTypeAbstract<TItem>
Expand Down
4 changes: 4 additions & 0 deletions src/ArrayType/ArrayOfObjects.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
/**
* Immutable collection of objects.
*
* Example
* - $v = ArrayOfObjects::fromItems(new \stdClass());
* $v->count(); // 1
*
* @template TItem of object
*
* @template-extends ArrayTypeAbstract<TItem>
Expand Down
187 changes: 187 additions & 0 deletions src/ArrayType/ArrayOfPrimitives.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

declare(strict_types=1);

namespace PhpTypedValues\ArrayType;

use PhpTypedValues\Base\ArrayType\ArrayTypeAbstract;
use PhpTypedValues\Base\Primitive\PrimitiveTypeInterface;
use PhpTypedValues\Exception\ArrayType\PrimitivesArrayTypeException;
use PhpTypedValues\Undefined\Alias\Undefined;
use Traversable;

use function count;

/**
* Immutable collection of primitives.
*
* Example
* - $v = ArrayOfPrimitives::fromArray([]);
* $v->isEmpty(); // true
* - $v = ArrayOfPrimitives::fromArray([IntegerPositive::fromInt(1)]);
* $v->isEmpty(); // false
*
* @template TItem of PrimitiveTypeInterface
*
* @template-extends ArrayTypeAbstract<TItem>
*
* @psalm-immutable
*/
readonly class ArrayOfPrimitives extends ArrayTypeAbstract
{
/**
* @var list<TItem>
*/
private array $value;

/**
* @param list<TItem> $value
*
* @throws PrimitivesArrayTypeException
*/
public function __construct(array $value)
{
foreach ($value as $item) {
if (!$item instanceof PrimitiveTypeInterface) {
throw new PrimitivesArrayTypeException('Expected array of PrimitiveTypeInterface instances');
}
}

$this->value = $value;
}

/**
* @return non-negative-int
*/
public function count(): int
{
return count($this->value);
}

/**
* @psalm-pure
*
* @param list<mixed> $value
*
* @throws PrimitivesArrayTypeException
*/
public static function fromArray(array $value): static
{
/** @var list<TItem> $value */
return new static($value);
}

/**
* @no-named-arguments
*
* @throws PrimitivesArrayTypeException
*/
public static function fromItems(PrimitiveTypeInterface ...$items): static
{
/** @var list<TItem> $items */
return new static($items);
}

/**
* @psalm-return list<TItem>
*/
public function getDefinedItems(): array
{
$result = [];

foreach ($this->value as $item) {
if (!$item->isUndefined()) {
$result[] = $item;
}
}

/** @var list<TItem> $result */
return $result;
}

/**
* @return Traversable<int, TItem>
*/
public function getIterator(): Traversable
{
yield from $this->value;
}

public function hasUndefined(): bool
{
foreach ($this->value as $item) {
if ($item instanceof Undefined) {
return true;
}
}

return false;
}

public function isEmpty(): bool
{
return $this->count() === 0;
}

public function isTypeOf(string ...$classNames): bool
{
foreach ($classNames as $className) {
if ($this instanceof $className) {
return true;
}
}

return false;
}

public function isUndefined(): bool
{
$items = $this->value;

if ($items === []) {
return false;
}

foreach ($items as $item) {
if (!$item->isUndefined()) {
return false;
}
}

return true;
}

/**
* JSON serialization helper.
*
* @throws PrimitivesArrayTypeException
*
* @psalm-mutation-free
*/
public function jsonSerialize(): array
{
return $this->toArray();
}

/**
* @psalm-mutation-free
*/
public function toArray(): array
{
$result = [];
foreach ($this->value as $item) {
/** @psalm-suppress ImpureMethodCall */
$result[] = $item->jsonSerialize();
}

return $result;
}

/**
* @return list<TItem>
*/
public function value(): array
{
return $this->value;
}
}
4 changes: 4 additions & 0 deletions src/ArrayType/ArrayUndefined.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
/**
* Immutable undefined collection.
*
* Example
* - $v = ArrayUndefined::create();
* $v->isEmpty(); // true
*
* @extends ArrayTypeAbstract<never>
*
* @psalm-immutable
Expand Down
11 changes: 11 additions & 0 deletions src/Exception/ArrayType/PrimitivesArrayTypeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace PhpTypedValues\Exception\ArrayType;

use PhpTypedValues\Exception\TypeException;

class PrimitivesArrayTypeException extends TypeException
{
}
135 changes: 135 additions & 0 deletions tests/Unit/ArrayType/ArrayOfPrimitivesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace PhpTypedValues\Tests\Unit\ArrayType;

use PhpTypedValues\ArrayType\ArrayOfPrimitives;
use PhpTypedValues\Exception\ArrayType\PrimitivesArrayTypeException;
use PhpTypedValues\Integer\Alias\IntegerType;
use PhpTypedValues\String\Alias\StringType;
use PhpTypedValues\Undefined\Alias\Undefined;
use stdClass;

covers(ArrayOfPrimitives::class);

describe('ArrayOfPrimitives', function () {
describe('Creation', function () {
describe('fromArray', function () {
it('constructs from a valid list of primitives and preserves order', function () {
$i1 = IntegerType::fromInt(1);
$s1 = StringType::fromString('A');

$c = ArrayOfPrimitives::fromArray([$i1, $s1]);

expect($c->value())
->toHaveCount(2)
->and($c->value()[0])->toBe($i1)
->and($c->value()[1])->toBe($s1);
});

it('throws when any item is not a primitive', function () {
expect(fn() => ArrayOfPrimitives::fromArray([1, new stdClass()]))
->toThrow(PrimitivesArrayTypeException::class, 'Expected array of PrimitiveTypeInterface instances');
});
});

describe('fromItems', function () {
it('creates instance from variadic arguments', function () {
$i1 = IntegerType::fromInt(1);
$s1 = StringType::fromString('A');

$array = ArrayOfPrimitives::fromItems($i1, $s1);

expect($array)->toBeInstanceOf(ArrayOfPrimitives::class)
->and($array->count())->toBe(2)
->and($array->value())->toBe([$i1, $s1]);
});
});
});

describe('Collection Methods', function () {
it('isEmpty() returns correct boolean', function (array $input, bool $expected) {
$c = new ArrayOfPrimitives($input);
expect($c->isEmpty())->toBe($expected);
})->with([
'empty' => [[], true],
'not empty' => [[IntegerType::fromInt(1)], false],
]);

it('count() returns correct number of items', function (array $input, int $expected) {
$c = new ArrayOfPrimitives($input);
expect($c->count())->toBe($expected);
})->with([
'empty' => [[], 0],
'three items' => [[IntegerType::fromInt(1), IntegerType::fromInt(2), IntegerType::fromInt(3)], 3],
]);
});

describe('Undefined Handling', function () {
it('hasUndefined() detects undefined', function () {
$array = new ArrayOfPrimitives([IntegerType::fromInt(1), Undefined::create()]);
expect($array->hasUndefined())->toBeTrue();
});

it('hasUndefined() returns false for no undefined', function () {
$array = new ArrayOfPrimitives([IntegerType::fromInt(1)]);
expect($array->hasUndefined())->toBeFalse();
});

it('isUndefined() returns true for all undefined items', function () {
$array = new ArrayOfPrimitives([Undefined::create(), Undefined::create()]);
expect($array->isUndefined())->toBeTrue();
});

it('isUndefined() returns false for empty array', function () {
$array = new ArrayOfPrimitives([]);
expect($array->isUndefined())->toBeFalse();
});

it('isUndefined() returns false for mixed undefined and defined', function () {
$array = new ArrayOfPrimitives([IntegerType::fromInt(1), Undefined::create()]);
expect($array->isUndefined())->toBeFalse();
});
});

describe('Accessors', function () {
it('getDefinedItems() returns only defined items', function () {
$items = [IntegerType::fromInt(1), Undefined::create(), IntegerType::fromInt(2)];
$array = new ArrayOfPrimitives($items);

expect($array->getDefinedItems())->toHaveCount(2)
->and($array->getDefinedItems()[0])->toBe($items[0])
->and($array->getDefinedItems()[1])->toBe($items[2]);
});

it('getIterator() iterates over all items', function () {
$items = [IntegerType::fromInt(1), IntegerType::fromInt(2)];
$array = new ArrayOfPrimitives($items);
$iterated = [];
foreach ($array as $item) {
$iterated[] = $item;
}
expect($iterated)->toBe($items);
});

it('isTypeOf() returns true for current class', function () {
$array = new ArrayOfPrimitives([]);
expect($array->isTypeOf(ArrayOfPrimitives::class))->toBeTrue();
});

it('isTypeOf() returns false for unknown class', function () {
$array = new ArrayOfPrimitives([]);
expect($array->isTypeOf(stdClass::class))->toBeFalse();
});

it('toArray() and jsonSerialize() return array representation', function () {
$i1 = IntegerType::fromInt(1);
$i2 = IntegerType::fromInt(2);
$array = new ArrayOfPrimitives([$i1, $i2]);

expect($array->toArray())->toBe([1, 2])
->and($array->jsonSerialize())->toBe([1, 2]);
});
});
});
Loading