Skip to content

Commit f1ea084

Browse files
authored
Merge pull request #55: implement AssertIterable methods
2 parents 4d1436a + c2df3d8 commit f1ea084

File tree

8 files changed

+188
-40
lines changed

8 files changed

+188
-40
lines changed

docs/guidelines

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/Api/Builtin/IterableType.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ interface IterableType
2121
* @param mixed $needle The value to look for within the iterable.
2222
* @param string $message Optional message for the assertion.
2323
* @throws AssertException when the assertion fails.
24-
*
25-
* @deprecated To be implemented
2624
*/
2725
public function contains(mixed $needle, string $message = ''): self;
2826

@@ -32,19 +30,24 @@ public function contains(mixed $needle, string $message = ''): self;
3230
* @param iterable $expected The iterable to compare size against.
3331
* @param string $message Optional message for the assertion.
3432
* @throws AssertException when the assertion fails.
35-
*
36-
* @deprecated To be implemented
3733
*/
3834
public function sameSizeAs(iterable $expected, string $message = ''): self;
3935

36+
/**
37+
* Asserts that the iterable has the expected number of elements.
38+
*
39+
* @param int $expected The expected count of elements.
40+
* @throws AssertException when the assertion fails.
41+
*/
42+
public function hasCount(int $expected): self;
43+
4044
/**
4145
* Asserts that all values in the iterable are of the specified type.
4246
*
43-
* @param non-empty-string $type The expected type name (e.g., 'int', 'string', 'object', class name).
47+
* @param non-empty-string $type The expected type name (e.g., 'int', 'string', 'object', class name)
48+
* considered valid by {@see \get_debug_type()}.
4449
* @param string $message Optional message for the assertion.
4550
* @throws AssertException when the assertion fails.
46-
*
47-
* @deprecated To be implemented
4851
*/
4952
public function allOf(string $type, string $message = ''): self;
5053
}

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+
*
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: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,126 @@
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+
*
14+
* @property iterable $value
1115
*/
1216
trait IterableTrait
1317
{
14-
/**
15-
* Asserts that the iterable contains the given needle.
16-
* @param mixed $needle The value to look for within the iterable.
17-
* @param string $message Optional message for the assertion.
18-
* @throws AssertException when the assertion fails.
19-
*/
18+
#[\Override]
2019
public function contains(mixed $needle, string $message = ''): self
2120
{
22-
throw new \LogicException('Not implemented yet');
21+
foreach ($this->value as $item) {
22+
if ($item === $needle) {
23+
StaticState::log('Assert contains: ' . Support::stringify($needle) . '.');
24+
return new self($this->value);
25+
}
26+
}
27+
28+
StaticState::fail(
29+
AssertException::fail(
30+
\sprintf(
31+
'Failed to assert that %s contains %s.',
32+
Support::stringify($this->value),
33+
Support::stringify($needle),
34+
),
35+
),
36+
);
2337
}
2438

25-
/**
26-
* Asserts that the iterable has the same number of elements as the expected iterable.
27-
* @param iterable $expected The iterable to compare size against.
28-
* @param string $message Optional message for the assertion.
29-
* @throws AssertException when the assertion fails.
30-
*/
39+
#[\Override]
3140
public function sameSizeAs(iterable $expected, string $message = ''): self
3241
{
33-
throw new \LogicException('Not implemented yet');
42+
if (self::countIterable($this->value) === self::countIterable($expected)) {
43+
StaticState::log('Assert same size as: ' . Support::stringify($expected) . '.');
44+
return new self($this->value);
45+
}
46+
47+
StaticState::fail(
48+
AssertException::fail(
49+
\sprintf(
50+
'Failed to assert that iterable %s has the same number of elements as %s.',
51+
Support::stringify($this->value),
52+
Support::stringify($expected),
53+
),
54+
),
55+
);
56+
}
57+
58+
#[\Override]
59+
public function allOf(string $type, string $message = ''): self
60+
{
61+
$type = \strtolower($type);
62+
$type = match ($type) {
63+
'integer' => 'int',
64+
'double' => 'float',
65+
'boolean' => 'bool',
66+
default => $type,
67+
};
68+
foreach ($this->value as $element) {
69+
$actualType = \strtolower(\get_debug_type($element));
70+
$actualType === $type or StaticState::fail(
71+
AssertException::fail(
72+
\sprintf(
73+
'Failed to assert that all elements of iterable %s have type %s (found %s instead).',
74+
Support::stringify($this->value),
75+
Support::stringify($type),
76+
Support::stringify($actualType),
77+
),
78+
),
79+
);
80+
}
81+
82+
StaticState::log(
83+
\sprintf(
84+
'Assert all elements are of type %s.',
85+
Support::stringify($type),
86+
),
87+
);
88+
return new self($this->value);
3489
}
3590

36-
public function allOf(string $type, string $message = ''): \Testo\Assert\Api\Builtin\IterableType
91+
#[\Override]
92+
public function hasCount(int $expected): self
3793
{
38-
throw new \LogicException('Not implemented yet');
94+
$count = self::countIterable($this->value);
95+
if ($count === $expected) {
96+
StaticState::log("Assert count: {$count}.");
97+
return new self($this->value);
98+
}
99+
100+
StaticState::fail(
101+
AssertException::fail(
102+
\sprintf(
103+
'Failed to assert that %s has %d elements (found %d instead).',
104+
Support::stringify($this->value),
105+
$expected,
106+
$count,
107+
),
108+
),
109+
);
110+
}
111+
112+
/**
113+
* Counts the number of elements in the given iterable.
114+
*/
115+
private static function countIterable(iterable $value): int
116+
{
117+
// if Countable
118+
if (\is_array($value) || $value instanceof \Countable) {
119+
return \count($value);
120+
}
121+
122+
// if Traversable
123+
$count = 0;
124+
foreach ($value as $_) {
125+
$count++;
126+
}
127+
128+
return $count;
39129
}
40130
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ trait NumericTrait
2323
public function greaterThan(int|float $min, string $message = ''): self
2424
{
2525
if ($this->value > $min) {
26-
StaticState::log('Assert `' . $this->value . '` is greater than `' . $min . '`', $message);
26+
StaticState::log('Assert `' . $this->value . ' > ' . $min . '`', $message);
2727
return $this;
2828
}
2929

3030
StaticState::fail(AssertException::compare(
3131
$min,
3232
$this->value,
3333
$message,
34-
pattern: 'Failed asserting that value `%2$s` is greater than `%1$s`.',
34+
pattern: 'Failed asserting that `%2$s > %1$s`.',
3535
showDiff: false,
3636
));
3737
}
@@ -46,15 +46,15 @@ public function greaterThan(int|float $min, string $message = ''): self
4646
public function greaterThanOrEqual(int|float $min, string $message = ''): self
4747
{
4848
if ($this->value >= $min) {
49-
StaticState::log('Assert `' . $this->value . '` is greater than or equal to `' . $min . '`', $message);
49+
StaticState::log('Assert `' . $this->value . ' >= ' . $min . '`', $message);
5050
return $this;
5151
}
5252

5353
StaticState::fail(AssertException::compare(
5454
$min,
5555
$this->value,
5656
$message,
57-
pattern: 'Failed asserting that value `%2$s` is greater than or equal to `%1$s`.',
57+
pattern: 'Failed asserting that `%2$s >= %1$s`.',
5858
showDiff: false,
5959
));
6060
}
@@ -69,15 +69,15 @@ public function greaterThanOrEqual(int|float $min, string $message = ''): self
6969
public function lessThan(int|float $max, string $message = ''): self
7070
{
7171
if ($this->value < $max) {
72-
StaticState::log('Assert `' . $this->value . '` is less than `' . $max . '`', $message);
72+
StaticState::log('Assert `' . $this->value . ' < ' . $max . '`', $message);
7373
return $this;
7474
}
7575

7676
StaticState::fail(AssertException::compare(
7777
$max,
7878
$this->value,
7979
$message,
80-
pattern: 'Failed asserting that value `%2$s` is less than `%1$s`.',
80+
pattern: 'Failed asserting that `%2$s < %1$s`.',
8181
showDiff: false,
8282
));
8383
}
@@ -92,15 +92,15 @@ public function lessThan(int|float $max, string $message = ''): self
9292
public function lessThanOrEqual(int|float $max, string $message = ''): self
9393
{
9494
if ($this->value <= $max) {
95-
StaticState::log('Assert `' . $this->value . '` is less than or equal to `' . $max . '`', $message);
95+
StaticState::log('Assert `' . $this->value . ' <= ' . $max . '`', $message);
9696
return $this;
9797
}
9898

9999
StaticState::fail(AssertException::compare(
100100
$max,
101101
$this->value,
102102
$message,
103-
pattern: 'Failed asserting that value `%2$s` is less than or equal to `%1$s`.',
103+
pattern: 'Failed asserting that `%2$s <= %1$s`.',
104104
showDiff: false,
105105
));
106106
}

src/Assert/State/AssertException.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,19 @@ public static function exceptionClass(
9090
);
9191
}
9292

93+
#[\Override]
9394
final public function isSuccess(): bool
9495
{
9596
return false;
9697
}
9798

99+
#[\Override]
98100
final public function getContext(): ?string
99101
{
100102
return $this->context !== '' ? $this->context : null;
101103
}
102104

105+
#[\Override]
103106
final public function __toString(): string
104107
{
105108
return $this->assertion;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Assert\Self;
6+
7+
use Testo\Assert;
8+
use Testo\Attribute\Test;
9+
use Testo\Expect;
10+
11+
/**
12+
* @see Assert::iterable()
13+
*/
14+
final class AssertIterable
15+
{
16+
#[Test]
17+
public function checkIterableType(): void
18+
{
19+
// This assertion checks incoming data type
20+
Assert::iterable(new \ArrayIterator([1, 2, 3]));
21+
Assert::iterable([]);
22+
}
23+
24+
#[Test]
25+
public function checkContains(): void
26+
{
27+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->contains(3);
28+
Assert::iterable([1, 2, 3])->contains(3);
29+
}
30+
31+
#[Test]
32+
public function checkSameSizeAs(): void
33+
{
34+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->sameSizeAs(new \ArrayIterator(['a', 'b', 'c']));
35+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->sameSizeAs(['a', 'b', 'c']);
36+
}
37+
38+
#[Test]
39+
public function assertCount(): void
40+
{
41+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->hasCount(3);
42+
}
43+
44+
#[Test]
45+
public function checkAllOf(): void
46+
{
47+
Assert::iterable(new \ArrayIterator([1, 2, 3]))->allOf('integer');
48+
Assert::iterable(['a', 'b', 'c'])->allOf('string');
49+
50+
Expect::exception(Assert\State\AssertException::class);
51+
Assert::iterable([true, false, 'true'])->allOf('bool');
52+
}
53+
}

0 commit comments

Comments
 (0)