diff --git a/src/Guardian.php b/src/Guardian.php index 82e1fcd..e6b6777 100644 --- a/src/Guardian.php +++ b/src/Guardian.php @@ -15,6 +15,7 @@ use Midnite81\Guardian\Exceptions\RulePreventsExecutionException; use Midnite81\Guardian\Helpers\Arrays; use Midnite81\Guardian\Helpers\RulesetPreparator; +use Midnite81\Guardian\Helpers\Str; use Midnite81\Guardian\Rules\ErrorHandlingRule; use Midnite81\Guardian\Rules\RateLimitRule; use Midnite81\Guardian\Rulesets\GenericErrorHandlingRuleset; @@ -124,7 +125,8 @@ public function setIdentifier(string $identifier, string $prefix = 'guardian'): throw new IdentifierCannotBeEmptyException('Identifier cannot be empty'); } - $safe = preg_replace('/[^a-zA-Z0-9_-]/', '', $identifier); + $safe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $identifier); + $safePrefix = preg_replace('/[^a-zA-Z0-9_-]/', '_', $prefix); if (empty($prefix)) { if (!preg_match('/^[a-zA-Z]/', $safe ?? '')) { @@ -136,7 +138,12 @@ public function setIdentifier(string $identifier, string $prefix = 'guardian'): throw new IdentifierCannotBeEmptyException('Identifier cannot be empty'); } - $this->identifier = ($prefix ? $prefix . '_' : '') . substr($safe, 0, 128); + $this->identifier = Str::of(($safePrefix ? $safePrefix . '_' : '') . substr($safe, 0, 128)) + ->removeDuplicateCharacters('_') + ->removeFinalCharIf('_') + ->toLower() + ->limit(100) + ->toString(); } /** diff --git a/src/Helpers/Str.php b/src/Helpers/Str.php new file mode 100644 index 0000000..a2d56c3 --- /dev/null +++ b/src/Helpers/Str.php @@ -0,0 +1,114 @@ +string; + } + + /** + * Convert the string to lowercase. + * + * @return $this + */ + public function toLower(): Str + { + $this->string = strtolower($this->string); + + return $this; + } + + /** + * Remove duplicate occurrences of specified character(s). + * + * @param string|array $characters The character(s) to remove duplicates of. + * @return $this + */ + public function removeDuplicateCharacters(string|array $characters = []): Str + { + if (is_string($characters)) { + $characters = [$characters]; + } + + $pattern = '/(' . implode('|', array_map('preg_quote', $characters, array_fill(0, count($characters), '/'))) . ')\1+/u'; + $this->string = (string) preg_replace($pattern, '$1', $this->string); + + return $this; + } + + /** + * Remove the final character if it matches the specified character. + * + * @param string $character The character to remove if it's at the end. + * @return $this + */ + public function removeFinalCharIf(string $character): Str + { + $this->string = rtrim($this->string, '_'); + + return $this; + } + + /** + * Limit the string to a specified number of characters. + * + * @param int $numberOfCharacters The maximum number of characters to keep. + * @return $this + */ + public function limit(int $numberOfCharacters): Str + { + $this->string = substr($this->string, 0, $numberOfCharacters); + + return $this; + } + + /** + * Modify the string using a custom callback function. + * + * @param callable $callback A function that takes a string and returns a modified string. + * @return $this + */ + public function modify(callable $callback): Str + { + $this->string = $callback($this->string); + + return $this; + } +} diff --git a/tests/GuardianTest.php b/tests/GuardianTest.php index bb90b8e..7db39da 100644 --- a/tests/GuardianTest.php +++ b/tests/GuardianTest.php @@ -232,13 +232,50 @@ ->toThrow(IdentifierCannotBeEmptyException::class, 'Identifier cannot be empty'); }); -it('must make safe an identifer starting with a number', function () { +it('must make safe an identifier starting with a number', function () { $guardian = new Guardian('test', new LaravelStore(app('cache.store'))); $guardian->setIdentifier('001', ''); expect($guardian->getIdentifier())->toBe('id_001'); }); +it('handles various identifier inputs', function () { + $guardian = new Guardian('test', new LaravelStore(app('cache.store'))); + + // Test with special characters + $guardian->setIdentifier('user@123_action/get'); + expect($guardian->getIdentifier())->toBe('guardian_user_123_action_get'); + + // Test with spaces + $guardian->setIdentifier('user with spaces'); + expect($guardian->getIdentifier())->toBe('guardian_user_with_spaces'); + + // Test with numbers only + $guardian->setIdentifier('12345'); + expect($guardian->getIdentifier())->toBe('guardian_12345'); + + // Test with uppercase letters + $guardian->setIdentifier('USER_UPPERCASE'); + expect($guardian->getIdentifier())->toBe('guardian_user_uppercase'); + + // Test with non-alphanumeric characters + $guardian->setIdentifier('user!@#$%^&*()'); + expect($guardian->getIdentifier())->toBe('guardian_user'); + + // Test with leading and trailing spaces + $guardian->setIdentifier(' trimmed_user '); + expect($guardian->getIdentifier())->toBe('guardian_trimmed_user'); + + // Test with very long identifier + $longIdentifier = str_repeat('a', 150); + $guardian->setIdentifier($longIdentifier); + expect($guardian->getIdentifier())->toBe('guardian_' . substr($longIdentifier, 0, 91)); + + // Test with a prefix + $guardian->setIdentifier('user', 'prefix/prefix'); + expect($guardian->getIdentifier())->toBe('prefix_prefix_user'); +}); + it('throws exception by default when no error rules are set', function () { $guardian = new Guardian('test', new LaravelStore(app('cache.store'))); diff --git a/tests/Helpers/StrTest.php b/tests/Helpers/StrTest.php new file mode 100644 index 0000000..47a8382 --- /dev/null +++ b/tests/Helpers/StrTest.php @@ -0,0 +1,60 @@ +toBeInstanceOf(Str::class); + expect($str->toString())->toBe('Hello World'); +}); + +it('converts string to lowercase', function () { + $str = Str::of('HELLO WORLD'); + expect($str->toLower()->toString())->toBe('hello world'); +}); + +it('removes duplicate characters', function () { + $str = Str::of('aabbccddee'); + expect($str->removeDuplicateCharacters(['a', 'b'])->toString())->toBe('abccddee'); + + $str = Str::of('aabbccddee'); + expect($str->removeDuplicateCharacters('a')->toString())->toBe('abbccddee'); + + $str = Str::of('a__b__c__'); + expect($str->removeDuplicateCharacters('_')->toString())->toBe('a_b_c_'); +}); + +it('removes final character if it matches', function () { + $str = Str::of('Hello_'); + expect($str->removeFinalCharIf('_')->toString())->toBe('Hello'); + + $str = Str::of('Hello'); + expect($str->removeFinalCharIf('_')->toString())->toBe('Hello'); +}); + +it('limits the string to specified number of characters', function () { + $str = Str::of('Hello World'); + expect($str->limit(5)->toString())->toBe('Hello'); + + $str = Str::of('Hi'); + expect($str->limit(5)->toString())->toBe('Hi'); +}); + +it('modifies the string using a custom callback', function () { + $str = Str::of('hello world'); + $result = $str->modify(function ($string) { + return strtoupper($string); + }); + expect($result->toString())->toBe('HELLO WORLD'); +}); + +it('chains multiple operations', function () { + $str = Str::of('HELLO__WORLD__'); + $result = $str->toLower() + ->removeDuplicateCharacters('_') + ->removeFinalCharIf('_') + ->limit(10); + expect($result->toString())->toBe('hello_worl'); +});