Skip to content

Commit 5540dce

Browse files
committed
Introduce Str and NonEmptyStr string types with strict validations
- Added `Str` and `NonEmptyStr` classes to represent string values with optional non-empty constraints. - Implemented `StrType` as an abstract base for string types, alongside its interface (`StrTypeInterface`). - Enhanced `Assert` class with `nonEmptyString` method for ensuring non-empty string inputs. - Included extensive unit tests for `Str`, `NonEmptyStr`, and `Assert::nonEmptyString` to validate behavior and edge cases. - Updated example usages and test utilities to incorporate new string types.
1 parent e6bf93d commit 5540dce

File tree

9 files changed

+214
-0
lines changed

9 files changed

+214
-0
lines changed

src/Code/Assert/Assert.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,16 @@ public static function integerish(mixed $value, string $message = ''): void
6363
throw new TypeException($message !== '' ? $message : 'Expected an "integerish" value');
6464
}
6565
}
66+
67+
/**
68+
* Assert that the given string is non-empty.
69+
*
70+
* @throws TypeException
71+
*/
72+
public static function nonEmptyString(string $value, string $message = ''): void
73+
{
74+
if ($value === '') {
75+
throw new TypeException($message !== '' ? $message : 'Value must be a non-empty string');
76+
}
77+
}
6678
}

src/Code/String/StrType.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\Code\String;
6+
7+
/**
8+
* @psalm-immutable
9+
*/
10+
abstract readonly class StrType implements StrTypeInterface
11+
{
12+
public function toString(): string
13+
{
14+
return $this->value();
15+
}
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\Code\String;
6+
7+
/**
8+
* @psalm-immutable
9+
*/
10+
interface StrTypeInterface
11+
{
12+
public function value(): string;
13+
14+
public static function fromString(string $value): self;
15+
16+
public function toString(): string;
17+
}

src/String/NonEmptyStr.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\String;
6+
7+
use PhpTypedValues\Code\Assert\Assert;
8+
use PhpTypedValues\Code\Exception\TypeException;
9+
use PhpTypedValues\Code\String\StrType;
10+
11+
/**
12+
* @psalm-immutable
13+
*/
14+
final readonly class NonEmptyStr extends StrType
15+
{
16+
/** @var non-empty-string */
17+
protected string $value;
18+
19+
/**
20+
* @throws TypeException
21+
*/
22+
public function __construct(string $value)
23+
{
24+
Assert::nonEmptyString($value, 'Value must be a non-empty string');
25+
26+
/** @var non-empty-string $value */
27+
$this->value = $value;
28+
}
29+
30+
/**
31+
* @throws TypeException
32+
*/
33+
public static function fromString(string $value): self
34+
{
35+
return new self($value);
36+
}
37+
38+
/** @return non-empty-string */
39+
public function value(): string
40+
{
41+
return $this->value;
42+
}
43+
}

src/String/Str.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\String;
6+
7+
use PhpTypedValues\Code\String\StrType;
8+
9+
/**
10+
* Represents any PHP string.
11+
*
12+
* @psalm-immutable
13+
*/
14+
final readonly class Str extends StrType
15+
{
16+
protected string $value;
17+
18+
public function __construct(string $value)
19+
{
20+
$this->value = $value;
21+
}
22+
23+
public static function fromString(string $value): self
24+
{
25+
return new self($value);
26+
}
27+
28+
public function value(): string
29+
{
30+
return $this->value;
31+
}
32+
}

src/psalmTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use PhpTypedValues\Integer\NonNegativeInt;
1313
use PhpTypedValues\Integer\PositiveInt;
1414
use PhpTypedValues\Integer\WeekDayInt;
15+
use PhpTypedValues\String\NonEmptyStr;
16+
use PhpTypedValues\String\Str;
1517

1618
/**
1719
* Integer.
@@ -23,6 +25,14 @@
2325

2426
echo Integer::fromString('10')->toString();
2527

28+
/**
29+
* String.
30+
*/
31+
testString(Str::fromString('hi')->value());
32+
testNonEmptyString(NonEmptyStr::fromString('hi')->value());
33+
34+
echo Str::fromString('hi')->toString();
35+
2636
/**
2737
* Artificial functions.
2838
*/
@@ -54,3 +64,16 @@ function testWeekDayInt(int $i): int
5464
{
5565
return $i;
5666
}
67+
68+
function testString(string $i): string
69+
{
70+
return $i;
71+
}
72+
73+
/**
74+
* @param non-empty-string $i
75+
*/
76+
function testNonEmptyString(string $i): string
77+
{
78+
return $i;
79+
}

tests/Unit/Code/Assert/AssertTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,20 @@
5353
Assert::integerish(5, '');
5454
expect(true)->toBeTrue();
5555
});
56+
57+
it('nonEmptyString throws with default message when empty', function (): void {
58+
expect(fn() => Assert::nonEmptyString('', ''))
59+
->toThrow(TypeException::class, 'Value must be a non-empty string');
60+
});
61+
62+
it('nonEmptyString throws with custom message when provided', function (): void {
63+
expect(fn() => Assert::nonEmptyString('', 'custom non-empty message'))
64+
->toThrow(TypeException::class, 'custom non-empty message');
65+
});
66+
67+
it('nonEmptyString does not throw for non-empty strings', function (): void {
68+
Assert::nonEmptyString('a', '');
69+
Assert::nonEmptyString(' ', '');
70+
Assert::nonEmptyString("\u{1F600}", '');
71+
expect(true)->toBeTrue();
72+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpTypedValues\String\Str;
6+
7+
it('fromString returns exact value and toString matches', function (): void {
8+
$s1 = Str::fromString('hello');
9+
expect($s1->value())->toBe('hello')
10+
->and($s1->toString())->toBe('hello');
11+
12+
$s2 = Str::fromString('');
13+
expect($s2->value())->toBe('')
14+
->and($s2->toString())->toBe('');
15+
});
16+
17+
it('handles unicode and whitespace transparently', function (): void {
18+
$unicode = Str::fromString('Привет 🌟');
19+
expect($unicode->value())->toBe('Привет 🌟')
20+
->and($unicode->toString())->toBe('Привет 🌟');
21+
22+
$ws = Str::fromString(' spaced ');
23+
expect($ws->value())->toBe(' spaced ')
24+
->and($ws->toString())->toBe(' spaced ');
25+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpTypedValues\Code\Exception\TypeException;
6+
use PhpTypedValues\String\NonEmptyStr;
7+
8+
it('constructs and preserves non-empty string', function (): void {
9+
$s = new NonEmptyStr('hello');
10+
expect($s->value())->toBe('hello')
11+
->and($s->toString())->toBe('hello');
12+
});
13+
14+
it('allows whitespace and unicode as non-empty', function (): void {
15+
$w = new NonEmptyStr(' ');
16+
$u = NonEmptyStr::fromString('🙂');
17+
expect($w->value())->toBe(' ')
18+
->and($u->toString())->toBe('🙂');
19+
});
20+
21+
it('throws on empty string via constructor', function (): void {
22+
expect(fn() => new NonEmptyStr(''))
23+
->toThrow(TypeException::class, 'Value must be a non-empty string');
24+
});
25+
26+
it('throws on empty string via fromString', function (): void {
27+
expect(fn() => NonEmptyStr::fromString(''))
28+
->toThrow(TypeException::class, 'Value must be a non-empty string');
29+
});

0 commit comments

Comments
 (0)