Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/Validator/Contains.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Utopia\Validator;

use Utopia\Validator;

/**
* Contains
*
* Validate that a string contains at least one of the predefined substrings.
*/
class Contains extends Validator
{
/**
* @var array
*/
protected array $patterns;

/**
* @var bool
*/
protected bool $strict;

/**
* Constructor
*
* Sets a list of substrings to search for and strict mode.
*
* @param array $patterns
* @param bool $strict enable case-sensitive matching
*/
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;
}

/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
$message = 'Value must contain one of ('.\implode(', ', $this->patterns).')';

if ($this->strict) {
$message .= ' (case-sensitive)';
} else {
$message .= ' (case-insensitive)';
}

return $message;
}

/**
* 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, 'UTF-8');
}

foreach ($this->patterns as $pattern) {
$pattern = $this->strict ? $pattern : \mb_strtolower($pattern, 'UTF-8');
Comment thread
Meldiron marked this conversation as resolved.

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;
}
}
129 changes: 129 additions & 0 deletions tests/Validator/ContainsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Utopia\Validator;

use PHPUnit\Framework\TestCase;
use stdClass;

class ContainsTest extends TestCase
{
public function testCanValidateWithSinglePattern(): void
{
$validator = new Contains(['[skip ci]']);

$this->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 testThrowsExceptionForEmptyPatternsArray(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Patterns array cannot be empty');

new Contains([]);
}

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());
$this->assertStringContainsString('case-insensitive', $validator->getDescription());

$validatorStrict = new Contains(['foo', 'bar'], true);

$this->assertStringContainsString('case-sensitive', $validatorStrict->getDescription());
}
}
Loading