Skip to content

Commit 8c36375

Browse files
committed
Add StringUuidV4 type with strict UUID v4 validation and comprehensive tests
- Introduced `StringUuidV4` class to validate and normalize UUID v4 values (RFC 4122). - Added detailed error messages for invalid UUID formats, incorrect versions, empty values, or invalid variants. - Implemented comprehensive unit tests covering valid UUID preservation, normalization, and various error scenarios.
1 parent 5fe2038 commit 8c36375

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/String/StringUuidV4.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\String;
6+
7+
use PhpTypedValues\Code\Exception\StringTypeException;
8+
use PhpTypedValues\Code\String\StrType;
9+
10+
use function preg_match;
11+
use function sprintf;
12+
use function strtolower;
13+
14+
/**
15+
* RFC 4122 version 4 (random).
16+
*
17+
* @psalm-immutable
18+
*/
19+
readonly class StringUuidV4 extends StrType
20+
{
21+
/** @var non-empty-string */
22+
protected string $value;
23+
24+
/**
25+
* @throws StringTypeException
26+
*/
27+
public function __construct(string $value)
28+
{
29+
// Normalize to lowercase for consistent representation
30+
$normalized = strtolower($value);
31+
32+
// Most popular UUID type: RFC 4122 version 4 (random)
33+
// Format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx (hex, case-insensitive)
34+
if ($normalized === '') {
35+
// Provide a distinct message for empty input to ensure mutation testing can distinguish the branch
36+
throw new StringTypeException(sprintf('Expected non-empty UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "%s"', $value));
37+
}
38+
39+
if (preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $normalized) !== 1) {
40+
throw new StringTypeException(sprintf('Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "%s"', $value));
41+
}
42+
43+
$this->value = $normalized;
44+
}
45+
46+
/**
47+
* @throws StringTypeException
48+
*/
49+
public static function fromString(string $value): static
50+
{
51+
return new static($value);
52+
}
53+
54+
/** @return non-empty-string */
55+
public function value(): string
56+
{
57+
return $this->value;
58+
}
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpTypedValues\Code\Exception\StringTypeException;
6+
use PhpTypedValues\String\StringUuidV4;
7+
8+
it('accepts a valid lowercase UUID v4 and preserves value', function (): void {
9+
$uuid = '550e8400-e29b-41d4-a716-446655440000';
10+
$s = new StringUuidV4($uuid);
11+
12+
expect($s->value())->toBe($uuid)
13+
->and($s->toString())->toBe($uuid);
14+
});
15+
16+
it('normalizes uppercase input to lowercase while preserving the UUID semantics', function (): void {
17+
$upper = '550E8400-E29B-41D4-A716-446655440000';
18+
$s = StringUuidV4::fromString($upper);
19+
20+
expect($s->value())->toBe('550e8400-e29b-41d4-a716-446655440000')
21+
->and($s->toString())->toBe('550e8400-e29b-41d4-a716-446655440000');
22+
});
23+
24+
it('throws on empty string', function (): void {
25+
expect(fn() => new StringUuidV4(''))
26+
->toThrow(StringTypeException::class, 'Expected non-empty UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got ""');
27+
});
28+
29+
it('throws when UUID version is not 4 (e.g., version 1)', function (): void {
30+
$v1 = '550e8400-e29b-11d4-a716-446655440000';
31+
expect(fn() => StringUuidV4::fromString($v1))
32+
->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $v1 . '"');
33+
});
34+
35+
it('throws when UUID variant nibble is invalid (must be 8,9,a,b)', function (): void {
36+
$badVariant = '550e8400-e29b-41d4-7716-446655440000';
37+
expect(fn() => new StringUuidV4($badVariant))
38+
->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $badVariant . '"');
39+
});
40+
41+
it('throws on invalid characters or format (non-hex character)', function (): void {
42+
$badChar = '550e8400-e29b-41d4-a716-44665544000g';
43+
expect(fn() => StringUuidV4::fromString($badChar))
44+
->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $badChar . '"');
45+
});

0 commit comments

Comments
 (0)