Skip to content

Commit aeb4a1d

Browse files
committed
feat(assert): implement AssertIterable methods
- add `AssertIterable::contains()` method. - add `AssertIterable::sameSizeAs()` method - add `AssertIterable::allOf()` method
1 parent 4d1436a commit aeb4a1d

File tree

5 files changed

+133
-14
lines changed

5 files changed

+133
-14
lines changed

src/Assert.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Testo\Assert\State\AssertTypeFailure;
2222
use Testo\Assert\StaticState;
2323
use Testo\Assert\Support;
24+
use Testo\Assert\Internal\Assertion\AssertIterable;
2425

2526
/**
2627
* Assertion utilities.
@@ -294,12 +295,10 @@ public static function json(string $actual): JsonAbstract
294295
* Does not work with Generators.
295296
*
296297
* @throws AssertTypeFailure
297-
*
298-
* @deprecated To be implemented
299298
*/
300299
public static function iterable(mixed $actual): IterableType
301300
{
302-
throw new \LogicException('Not implemented yet');
301+
return AssertIterable::validateAndCreate($actual);
303302
}
304303

305304
/**

src/Assert/Internal/Assertion/AssertIterable.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ public function __construct(
2424
) {}
2525

2626
/**
27-
* Validate that the given value is a float and return an AssertFloat instance.
27+
* Validate that the given value is an iterable and return an AssertIterable instance.
2828
*
29-
* @param mixed $value The value to be asserted as float.
30-
* @return self An instance of AssertFloat.
31-
* @throws AssertTypeFailure when the value is not a float.
29+
* @param mixed $value The value to be asserted as an iterable.
30+
* @return self An instance of AssertIterable.
31+
* @throws AssertTypeFailure when the value is not an iterable.
3232
*/
3333
public static function validateAndCreate(mixed $value): self
3434
{

src/Assert/Internal/Assertion/Traits/IterableTrait.php

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,89 @@
55
namespace Testo\Assert\Internal\Assertion\Traits;
66

77
use Testo\Assert\State\AssertException;
8+
use Testo\Assert\StaticState;
9+
use Testo\Assert\Support;
810

911
/**
10-
* Contains methods for comparing numeric values
12+
* Contains assertion methods for iterable values.
13+
* @property iterable $value
1114
*/
1215
trait IterableTrait
1316
{
1417
/**
15-
* Asserts that the iterable contains the given needle.
18+
* Asserts that the iterable contains the given needle (strict comparison).
1619
* @param mixed $needle The value to look for within the iterable.
1720
* @param string $message Optional message for the assertion.
21+
*
1822
* @throws AssertException when the assertion fails.
1923
*/
2024
public function contains(mixed $needle, string $message = ''): self
2125
{
22-
throw new \LogicException('Not implemented yet');
26+
foreach ($this->value as $item) {
27+
if ($item === $needle) {
28+
StaticState::log(
29+
'Assert that iterable: ' . Support::stringify($this->value) . ' contains ' . Support::stringify($needle),
30+
);
31+
return new self($this->value);
32+
}
33+
}
34+
StaticState::fail(
35+
AssertException::fail(
36+
'Failed to assert that iterable ' . Support::stringify($this->value) . ' contains ' . Support::stringify($needle),
37+
),
38+
);
2339
}
2440

2541
/**
2642
* Asserts that the iterable has the same number of elements as the expected iterable.
43+
*
2744
* @param iterable $expected The iterable to compare size against.
2845
* @param string $message Optional message for the assertion.
29-
* @throws AssertException when the assertion fails.
46+
*
47+
* @throws AssertException When the iterables do not have the same size.
3048
*/
3149
public function sameSizeAs(iterable $expected, string $message = ''): self
3250
{
33-
throw new \LogicException('Not implemented yet');
51+
if (Support::countIterable($this->value) === Support::countIterable($expected)) {
52+
StaticState::log(
53+
'Assert that iterable: ' . Support::stringify($this->value) . ' has the same number of elements as ' . Support::stringify($expected),
54+
);
55+
return new self($this->value);
56+
}
57+
StaticState::fail(
58+
AssertException::fail(
59+
'Failed to assert that iterable ' . Support::stringify($this->value) . ' has the same number of elements as ' . Support::stringify($expected),
60+
),
61+
);
3462
}
3563

36-
public function allOf(string $type, string $message = ''): \Testo\Assert\Api\Builtin\IterableType
64+
/**
65+
* Asserts that all elements of the iterable have the given PHP type.
66+
*
67+
* The $type parameter uses PHP internal type names (compatible with gettype()),
68+
* e.g.: "integer", "double", "boolean", "string", "array", "object", "resource", "null".
69+
*
70+
* @param non-empty-string $type Expected PHP type name for all elements.
71+
* @param string $message Optional message for the assertion.
72+
*
73+
* @throws AssertException When at least one element has a different type.
74+
*/
75+
public function allOf(string $type, string $message = ''): self
3776
{
38-
throw new \LogicException('Not implemented yet');
77+
foreach ($this->value as $element) {
78+
$actualType = \gettype($element);
79+
if ($actualType !== $type) {
80+
StaticState::fail(
81+
AssertException::fail(
82+
'Failed to assert that all elements of iterable ' . Support::stringify($this->value) . ' have type ' . Support::stringify($type) .
83+
' (found ' . Support::stringify($actualType) . ' instead)',
84+
),
85+
);
86+
}
87+
}
88+
StaticState::log(
89+
'Assert that all elements of iterable ' . Support::stringify($this->value) . ' have type ' . Support::stringify($type),
90+
);
91+
return new self($this->value);
3992
}
4093
}

src/Assert/Support.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,23 @@ public static function stringify(mixed $value): string
2424
default => (string) $value,
2525
};
2626
}
27+
28+
/**
29+
* Counts the number of elements in the given iterable.
30+
*/
31+
public static function countIterable(iterable $value): int
32+
{
33+
// if Countable
34+
if (\is_array($value) || $value instanceof \Countable) {
35+
return \count($value);
36+
}
37+
38+
// if Traversable
39+
$count = 0;
40+
foreach ($value as $_) {
41+
$count++;
42+
}
43+
44+
return $count;
45+
}
2746
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Assert\Self;
6+
7+
use Testo\Assert;
8+
use Testo\Attribute\ExpectException;
9+
use Testo\Attribute\Test;
10+
use Testo\Expect;
11+
12+
/**
13+
* @see Assert::iterable()
14+
*/
15+
final class AssertIterable
16+
{
17+
#[Test]
18+
public function checkIterableType(): void
19+
{
20+
// This assertion checks incoming data type
21+
Assert::iterable(new \ArrayIterator([1, 2, 3]));
22+
Assert::iterable([]);
23+
}
24+
25+
#[Test]
26+
public function checkContains(): void
27+
{
28+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->contains(3);
29+
Assert::iterable([1, 2, 3])->contains(3);
30+
}
31+
32+
#[Test]
33+
public function checkSameSizeAs(): void
34+
{
35+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->sameSizeAs(new \ArrayIterator(['a', 'b', 'c']));
36+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->sameSizeAs(['a', 'b', 'c']);
37+
}
38+
39+
#[Test]
40+
public function checkAllOf(): void
41+
{
42+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->allOf('integer');
43+
Assert::iterable(['a', 'b', 'c'])->allOf('string');
44+
45+
Expect::exception(Assert\State\AssertException::class);
46+
Assert::iterable([true, false, 'true'])->allOf('bool');
47+
}
48+
}

0 commit comments

Comments
 (0)