Skip to content

Commit e9603b7

Browse files
authored
feat: add isValid method for basic validation (#274)
Signed-off-by: Alan Brault <alan.brault@visus.io>
1 parent 544424f commit e9603b7

File tree

3 files changed

+272
-4
lines changed

3 files changed

+272
-4
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ composer require visus/cuid2
2424
> Consider installing/enabling the PHP extension [GMP](https://www.php.net/manual/en/intro.gmp.php).
2525
> If this is not an option then [markrogoyski/math-php](https://github.com/markrogoyski/math-php) will be used as a fallback.
2626
27-
## Quick Example
27+
## Examples
2828

2929
### Instance Based
3030

@@ -64,3 +64,17 @@ echo $cuid->toString(); // apr5hhh4ox45krsg9gycbs9k
6464
$cuid = Visus\Cuid2\Cuid2::generate(10);
6565
echo $cuid; // pekw02xwsd
6666
```
67+
### Validation
68+
69+
> [!NOTE]
70+
> This method does not guarantee that the value is actually a CUID2, only that it follows the format.
71+
72+
```php
73+
<?php
74+
require_once 'vendor/autoload.php';
75+
76+
Cuid2::isValid('apr5hhh4ox45krsg9gycbs9k'); // true
77+
Cuid2::isValid('invalid-cuid'); // false
78+
Cuid2::isValid('pekw02xwsd', expectedLength: 10); // true
79+
80+
```

src/Cuid2.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ public static function generate(int $maxLength = 24): Cuid2
7878
return new self($maxLength);
7979
}
8080

81+
/**
82+
* Validates whether the given string is a valid CUID2 identifier.
83+
*
84+
* Function does not guarantee that the provided value is a CUID2 generated by this library,
85+
* only that it conforms to the CUID2 format.
86+
*
87+
* @param string $id The CUID2 identifier to validate.
88+
* @param int|null $expectedLength The expected length of the CUID2 identifier.
89+
* If null, lengths of 4 to 32 characters are checked.
90+
*
91+
* @return bool True if the identifier is valid, false otherwise.
92+
*/
93+
public static function isValid(string $id, ?int $expectedLength = null): bool
94+
{
95+
$length = strlen($id);
96+
97+
$isLengthValid = ($length >= 4 && $length <= 32) &&
98+
($expectedLength === null ||
99+
($expectedLength >= 4 && $expectedLength <= 32 && $length === $expectedLength));
100+
101+
if (!$isLengthValid) {
102+
return false;
103+
}
104+
105+
$pattern = $expectedLength !== null
106+
? sprintf('/^[a-z][a-z0-9]{%d}$/', $expectedLength - 1)
107+
: '/^[a-z][a-z0-9]{3,31}$/';
108+
109+
return preg_match($pattern, $id) === 1;
110+
}
111+
81112
/**
82113
* @throws Exception
83114
*/

tests/Cuid2Test.php

Lines changed: 226 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,46 @@ public static function validLengthProvider(): array
4343
];
4444
}
4545

46+
/**
47+
* Provides valid CUID2 strings for testing isValid method.
48+
*
49+
* @return array<string, array<mixed>>
50+
*/
51+
public static function validCuidProvider(): array
52+
{
53+
return [
54+
'minimum length' => ['a1b2', 4],
55+
'short cuid' => ['x7z9k2m1', 8],
56+
'medium cuid' => ['q5w8e3r6t9y2u1i4', 16],
57+
'default length' => ['p0o9i8u7y6t5r4e3w2q1a2s3', 24],
58+
'maximum length' => ['z1x2c3v4b5n6m7q8w9e0r1t2y3u4i5o6', 32],
59+
'mixed characters' => ['a1b2c3d4e5f6g7h8i9j0k1l2', 24],
60+
'all letters' => ['abcd', 4],
61+
'starts with letter and numbers' => ['a123', 4],
62+
];
63+
}
64+
65+
/**
66+
* Provides invalid CUID2 strings for testing isValid method.
67+
*
68+
* @return array<string, array<mixed>>
69+
*/
70+
public static function invalidCuidProvider(): array
71+
{
72+
return [
73+
'too short' => ['abc', null],
74+
'too long' => ['a' . str_repeat('1', 32), null],
75+
'empty string' => ['', null],
76+
'contains uppercase' => ['A1b2c3d4', null],
77+
'contains special chars' => ['a1b2-c3d4', null],
78+
'contains spaces' => ['a1b2 c3d4', null],
79+
'contains underscore' => ['a1b2_c3d4', null],
80+
'starts with number' => ['1abc', null],
81+
'unicode characters' => ['a1b2ñ3d4', null],
82+
'contains symbols' => ['a1b2@3d4', null],
83+
];
84+
}
85+
4686
/**
4787
* Tests that the generated CUID2 contains only valid base36 characters.
4888
*
@@ -468,9 +508,192 @@ public function testConstructorAndGenerateMethodEquivalence(): void
468508
$generateResult = (string)$generateCuid;
469509

470510
// Should have same length and format, but different values
471-
$this->assertEquals(strlen($constructorResult), strlen($generateResult), 'Both methods should produce same length');
472-
$this->assertMatchesRegularExpression('/^[a-z][0-9a-z]*$/', $constructorResult, 'Constructor result should have correct format');
473-
$this->assertMatchesRegularExpression('/^[a-z][0-9a-z]*$/', $generateResult, 'Generate result should have correct format');
511+
$this->assertEquals(strlen($constructorResult),
512+
strlen($generateResult),
513+
'Both methods should produce same length'
514+
);
515+
516+
$this->assertMatchesRegularExpression('/^[a-z][0-9a-z]*$/',
517+
$constructorResult,
518+
'Constructor result should have correct format'
519+
);
520+
521+
$this->assertMatchesRegularExpression('/^[a-z][0-9a-z]*$/',
522+
$generateResult,
523+
'Generate result should have correct format'
524+
);
525+
474526
$this->assertNotEquals($constructorResult, $generateResult, 'Results should be unique');
475527
}
528+
529+
/**
530+
* Tests isValid method with valid CUID2 strings.
531+
*
532+
* @dataProvider validCuidProvider
533+
*/
534+
public function testIsValidWithValidCuids(string $cuid, ?int $expectedLength = null): void
535+
{
536+
$this->assertTrue(
537+
Cuid2::isValid($cuid, $expectedLength),
538+
"CUID '$cuid' should be considered valid"
539+
);
540+
}
541+
542+
/**
543+
* Tests isValid method with invalid CUID2 strings.
544+
*
545+
* @dataProvider invalidCuidProvider
546+
*/
547+
public function testIsValidWithInvalidCuids(string $cuid, ?int $expectedLength = null): void
548+
{
549+
$this->assertFalse(
550+
Cuid2::isValid($cuid, $expectedLength),
551+
"CUID '$cuid' should be considered invalid"
552+
);
553+
}
554+
555+
/**
556+
* Tests isValid method with length validation.
557+
*/
558+
public function testIsValidWithExpectedLength(): void
559+
{
560+
$validCuid = 'a1b2c3d4e5f6g7h8';
561+
562+
// Should be valid when length matches
563+
$this->assertTrue(
564+
Cuid2::isValid($validCuid, 16),
565+
'Valid CUID should pass when expected length matches actual length'
566+
);
567+
568+
// Should be invalid when length doesn't match
569+
$this->assertFalse(
570+
Cuid2::isValid($validCuid, 24),
571+
'Valid CUID should fail when expected length differs from actual length'
572+
);
573+
574+
$this->assertFalse(
575+
Cuid2::isValid($validCuid, 8),
576+
'Valid CUID should fail when expected length is shorter than actual length'
577+
);
578+
}
579+
580+
/**
581+
* Tests isValid method with edge cases for length validation.
582+
*/
583+
public function testIsValidLengthEdgeCases(): void
584+
{
585+
// Test minimum valid length
586+
$this->assertTrue(Cuid2::isValid('a1b2', 4), 'Minimum length CUID should be valid');
587+
$this->assertFalse(Cuid2::isValid('a1b', 3), 'Below minimum length should be invalid even with matching expected length');
588+
589+
// Test maximum valid length
590+
$maxLengthCuid = 'a' . str_repeat('1', 31);
591+
$this->assertTrue(Cuid2::isValid($maxLengthCuid, 32), 'Maximum length CUID should be valid');
592+
593+
// Test above maximum length
594+
$tooLongCuid = 'a' . str_repeat('1', 32);
595+
$this->assertFalse(Cuid2::isValid($tooLongCuid, 33), 'Above maximum length should be invalid even with matching expected length');
596+
}
597+
598+
/**
599+
* Tests isValid method without expected length parameter.
600+
*/
601+
public function testIsValidWithoutExpectedLength(): void
602+
{
603+
$this->assertTrue(Cuid2::isValid('a1b2'), 'Valid minimum length CUID should pass without expected length');
604+
$this->assertTrue(Cuid2::isValid('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'), 'Valid maximum length CUID should pass without expected length');
605+
$this->assertFalse(Cuid2::isValid('abc'), 'Too short CUID should fail without expected length');
606+
$this->assertFalse(Cuid2::isValid('A1b2'), 'CUID with uppercase should fail without expected length');
607+
}
608+
609+
/**
610+
* Tests isValid method with generated CUIDs.
611+
*
612+
* @throws Exception
613+
*/
614+
public function testIsValidWithGeneratedCuids(): void
615+
{
616+
$lengths = [4, 8, 16, 24, 32];
617+
618+
foreach ($lengths as $length) {
619+
$cuid = Cuid2::generate($length);
620+
$cuidString = (string)$cuid;
621+
622+
// Generated CUID should always be valid
623+
$this->assertTrue(
624+
Cuid2::isValid($cuidString),
625+
"Generated CUID of length $length should be valid"
626+
);
627+
628+
// Generated CUID should be valid with correct expected length
629+
$this->assertTrue(
630+
Cuid2::isValid($cuidString, $length),
631+
"Generated CUID should be valid with matching expected length $length"
632+
);
633+
634+
// Generated CUID should be invalid with wrong expected length
635+
$wrongLength = $length === 4 ? 8 : 4;
636+
$this->assertFalse(
637+
Cuid2::isValid($cuidString, $wrongLength),
638+
"Generated CUID should be invalid with wrong expected length $wrongLength"
639+
);
640+
}
641+
}
642+
643+
/**
644+
* Tests isValid method with character set validation.
645+
*/
646+
public function testIsValidCharacterSetValidation(): void
647+
{
648+
// Test all valid base36 characters
649+
$validChars = '0123456789abcdefghijklmnopqrstuvwxyz';
650+
$testCuid = 'a' . substr(str_shuffle($validChars), 0, 7);
651+
$this->assertTrue(Cuid2::isValid($testCuid), 'CUID with all valid base36 characters should be valid');
652+
653+
// Test invalid characters
654+
$invalidChars = ['A', 'Z', '-', '_', '@', '#', '$', '%', '^', '&', '*'];
655+
foreach ($invalidChars as $invalidChar) {
656+
$invalidCuid = 'a1b2' . $invalidChar . '3d4';
657+
$this->assertFalse(
658+
Cuid2::isValid($invalidCuid),
659+
"CUID containing invalid character '$invalidChar' should be invalid"
660+
);
661+
}
662+
}
663+
664+
/**
665+
* Tests isValid method regex pattern matching.
666+
*/
667+
public function testIsValidRegexPattern(): void
668+
{
669+
// Test the regex pattern directly through various scenarios
670+
$validPatterns = [
671+
'a123',
672+
'z999',
673+
'x1y2z3a4b5c6',
674+
'q0w1e2r3t4y5u6i7o8p9',
675+
];
676+
677+
foreach ($validPatterns as $pattern) {
678+
$this->assertTrue(
679+
Cuid2::isValid($pattern),
680+
"Pattern '$pattern' should match valid CUID regex"
681+
);
682+
}
683+
684+
$invalidPatterns = [
685+
'A123', // uppercase
686+
'a-23', // contains hyphen
687+
'a_23', // contains underscore
688+
'a 23', // contains space
689+
'a.23', // contains dot
690+
];
691+
692+
foreach ($invalidPatterns as $pattern) {
693+
$this->assertFalse(
694+
Cuid2::isValid($pattern),
695+
"Pattern '$pattern' should not match valid CUID regex"
696+
);
697+
}
698+
}
476699
}

0 commit comments

Comments
 (0)