From 3e0e0c5bbc00181a3f3e325478a6e79a59cb85f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 09:49:24 +0200 Subject: [PATCH 1/5] Add contains validator --- src/Validator/Contains.php | 102 +++++++++++++++++++++++++ tests/Validator/ContainsTest.php | 124 +++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/Validator/Contains.php create mode 100644 tests/Validator/ContainsTest.php diff --git a/src/Validator/Contains.php b/src/Validator/Contains.php new file mode 100644 index 0000000..237d08b --- /dev/null +++ b/src/Validator/Contains.php @@ -0,0 +1,102 @@ +patterns = $patterns; + $this->strict = $strict; + } + + /** + * Get Description + * + * Returns validator description + * + * @return string + */ + public function getDescription(): string + { + return 'Value must contain one of ('.\implode(', ', $this->patterns).')'; + } + + /** + * Is valid + * + * Validation will pass when $value contains at least one of the patterns. + * + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + if (!\is_string($value)) { + return false; + } + + if (!$this->strict) { + $value = \mb_strtolower($value); + } + + foreach ($this->patterns as $pattern) { + $pattern = $this->strict ? $pattern : \mb_strtolower($pattern); + + if (\str_contains($value, $pattern)) { + return true; + } + } + + return false; + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/tests/Validator/ContainsTest.php b/tests/Validator/ContainsTest.php new file mode 100644 index 0000000..7bd1b97 --- /dev/null +++ b/tests/Validator/ContainsTest.php @@ -0,0 +1,124 @@ +assertTrue($validator->isValid('[skip ci] update changelog')); + $this->assertTrue($validator->isValid('docs: update readme [skip ci]')); + $this->assertTrue($validator->isValid('prefix[skip ci]suffix')); + + $this->assertFalse($validator->isValid('fix: real bug fix')); + $this->assertFalse($validator->isValid('skip deploy without brackets')); + $this->assertFalse($validator->isValid('')); + } + + public function testCanValidateWithMultiplePatterns(): void + { + $validator = new Contains(['[skip ci]', '[no ci]', '[ci skip]']); + + $this->assertTrue($validator->isValid('[skip ci]')); + $this->assertTrue($validator->isValid('[no ci]')); + $this->assertTrue($validator->isValid('[ci skip]')); + $this->assertFalse($validator->isValid('[skip deploy]')); + } + + public function testCanValidateLoosely(): void + { + $validator = new Contains(['[skip ci]']); + + $this->assertTrue($validator->isValid('[skip ci]')); + $this->assertTrue($validator->isValid('[SKIP CI]')); + $this->assertTrue($validator->isValid('[Skip Ci]')); + $this->assertTrue($validator->isValid('Docs update [SKIP CI]')); + } + + public function testCanValidateStrictly(): void + { + $validator = new Contains(['[skip ci]'], true); + + $this->assertTrue($validator->isValid('[skip ci]')); + $this->assertTrue($validator->isValid('prefix[skip ci]suffix')); + + $this->assertFalse($validator->isValid('[SKIP CI]')); + $this->assertFalse($validator->isValid('[Skip Ci]')); + } + + public function testCanValidateMultilineStrings(): void + { + $validator = new Contains(['[skip ci]']); + + $message = "feat: add new stuff\n\nMore detail here.\n\n[skip ci]"; + $this->assertTrue($validator->isValid($message)); + } + + public function testCanValidateWithEmptyPatternsArray(): void + { + $validator = new Contains([]); + + $this->assertFalse($validator->isValid('any string')); + $this->assertFalse($validator->isValid('')); + } + + public function testCanValidateWithEmptyPatternString(): void + { + $validator = new Contains(['']); + + $this->assertTrue($validator->isValid('any string')); + $this->assertTrue($validator->isValid('')); + } + + public function testCanValidateWithNonStringValues(): void + { + $validator = new Contains(['[skip ci]']); + + $this->assertFalse($validator->isValid(null)); + $this->assertFalse($validator->isValid([])); + $this->assertFalse($validator->isValid(123)); + $this->assertFalse($validator->isValid(12.34)); + $this->assertFalse($validator->isValid(true)); + $this->assertFalse($validator->isValid(false)); + $this->assertFalse($validator->isValid(new stdClass())); + } + + public function testCanValidatePartialMatches(): void + { + $validator = new Contains(['skip']); + + $this->assertTrue($validator->isValid('skip')); + $this->assertTrue($validator->isValid('skip ci')); + $this->assertTrue($validator->isValid('please skip this')); + $this->assertTrue($validator->isValid('skipping')); + + $this->assertFalse($validator->isValid('ski')); + $this->assertFalse($validator->isValid('')); + } + + public function testCanValidateWithUnicodeCharacters(): void + { + $validator = new Contains(['café', 'naïve']); + + $this->assertTrue($validator->isValid('I love café')); + $this->assertTrue($validator->isValid('Naïve approach')); + $this->assertTrue($validator->isValid('CAFÉ')); + + $this->assertFalse($validator->isValid('I love coffee')); + } + + public function testReturnsCorrectMetadata(): void + { + $validator = new Contains(['foo', 'bar']); + + $this->assertFalse($validator->isArray()); + $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); + $this->assertStringContainsString('foo', $validator->getDescription()); + $this->assertStringContainsString('bar', $validator->getDescription()); + } +} From 95a3d2933eb746c49682ea36ddcf7095760013a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 09:54:17 +0200 Subject: [PATCH 2/5] Update src/Validator/Contains.php Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- src/Validator/Contains.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Validator/Contains.php b/src/Validator/Contains.php index 237d08b..6d5cfb8 100644 --- a/src/Validator/Contains.php +++ b/src/Validator/Contains.php @@ -62,11 +62,11 @@ public function isValid($value): bool } if (!$this->strict) { - $value = \mb_strtolower($value); + $value = \mb_strtolower($value, 'UTF-8'); } foreach ($this->patterns as $pattern) { - $pattern = $this->strict ? $pattern : \mb_strtolower($pattern); + $pattern = $this->strict ? $pattern : \mb_strtolower($pattern, 'UTF-8'); if (\str_contains($value, $pattern)) { return true; From 15012b5213f9b85b53831fa9c3c0b6fa9aee75f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 09:56:57 +0200 Subject: [PATCH 3/5] Improve description --- src/Validator/Contains.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Validator/Contains.php b/src/Validator/Contains.php index 237d08b..bc93476 100644 --- a/src/Validator/Contains.php +++ b/src/Validator/Contains.php @@ -44,7 +44,15 @@ public function __construct(array $patterns, bool $strict = false) */ public function getDescription(): string { - return 'Value must contain one of ('.\implode(', ', $this->patterns).')'; + $message = 'Value must contain one of ('.\implode(', ', $this->patterns).')'; + + if ($this->strict) { + $message .= ' (case-sensitive)'; + } else { + $message .= ' (case-insensitive)'; + } + + return $message; } /** From 01adf8021896a71052fa8ceea4c34552ea17e05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 09:59:02 +0200 Subject: [PATCH 4/5] Fix empty patterm array behaviour --- src/Validator/Contains.php | 4 ++++ tests/Validator/ContainsTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Validator/Contains.php b/src/Validator/Contains.php index bc93476..c3e6d86 100644 --- a/src/Validator/Contains.php +++ b/src/Validator/Contains.php @@ -31,6 +31,10 @@ class Contains extends Validator */ public function __construct(array $patterns, bool $strict = false) { + if (empty($patterns)) { + throw new \InvalidArgumentException('Patterns array cannot be empty'); + } + $this->patterns = $patterns; $this->strict = $strict; } diff --git a/tests/Validator/ContainsTest.php b/tests/Validator/ContainsTest.php index 7bd1b97..4d5e10c 100644 --- a/tests/Validator/ContainsTest.php +++ b/tests/Validator/ContainsTest.php @@ -59,12 +59,12 @@ public function testCanValidateMultilineStrings(): void $this->assertTrue($validator->isValid($message)); } - public function testCanValidateWithEmptyPatternsArray(): void + public function testThrowsExceptionForEmptyPatternsArray(): void { - $validator = new Contains([]); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Patterns array cannot be empty'); - $this->assertFalse($validator->isValid('any string')); - $this->assertFalse($validator->isValid('')); + new Contains([]); } public function testCanValidateWithEmptyPatternString(): void From de406ca38c1aa43860d8d6c49bb93b02f0b170b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 14 May 2026 10:00:44 +0200 Subject: [PATCH 5/5] Improve metadata test assertions --- tests/Validator/ContainsTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Validator/ContainsTest.php b/tests/Validator/ContainsTest.php index 4d5e10c..749b399 100644 --- a/tests/Validator/ContainsTest.php +++ b/tests/Validator/ContainsTest.php @@ -120,5 +120,10 @@ public function testReturnsCorrectMetadata(): void $this->assertSame(\Utopia\Validator::TYPE_STRING, $validator->getType()); $this->assertStringContainsString('foo', $validator->getDescription()); $this->assertStringContainsString('bar', $validator->getDescription()); + $this->assertStringContainsString('case-insensitive', $validator->getDescription()); + + $validatorStrict = new Contains(['foo', 'bar'], true); + + $this->assertStringContainsString('case-sensitive', $validatorStrict->getDescription()); } }