Skip to content

Commit

Permalink
Merge "REST: Create validation and deserialization for SetPropertyDes…
Browse files Browse the repository at this point in the history
…cription"
  • Loading branch information
jenkins-bot authored and Gerrit Code Review committed Oct 11, 2023
2 parents 2ef770c + b7bb009 commit f220947
Show file tree
Hide file tree
Showing 6 changed files with 480 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare( strict_types=1 );

namespace Wikibase\Repo\RestApi\Application\UseCaseRequestValidation;

/**
* @license GPL-2.0-or-later
*/
interface PropertyDescriptionEditRequest extends PropertyIdRequest, LanguageCodeRequest {
public function getDescription(): string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare( strict_types=1 );

namespace Wikibase\Repo\RestApi\Application\UseCaseRequestValidation;

use LogicException;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\Term\Term;
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
use Wikibase\Repo\RestApi\Application\Validation\PropertyDescriptionValidator;

/**
* @license GPL-2.0-or-later
*/
class PropertyDescriptionEditRequestValidatingDeserializer {

private PropertyDescriptionValidator $validator;

public function __construct( PropertyDescriptionValidator $validator ) {
$this->validator = $validator;
}

/**
* @throws UseCaseError
*/
public function validateAndDeserialize( PropertyDescriptionEditRequest $request ): Term {
$language = $request->getLanguageCode();
$validationError = $this->validator->validate(
new NumericPropertyId( $request->getPropertyId() ),
$language,
$request->getDescription()
);

if ( $validationError ) {
$errorCode = $validationError->getCode();
$context = $validationError->getContext();
switch ( $errorCode ) {
case PropertyDescriptionValidator::CODE_INVALID:
throw new UseCaseError(
UseCaseError::INVALID_DESCRIPTION,
"Not a valid description: {$context[PropertyDescriptionValidator::CONTEXT_DESCRIPTION]}"
);
case PropertyDescriptionValidator::CODE_EMPTY:
throw new UseCaseError(
UseCaseError::DESCRIPTION_EMPTY,
'Description must not be empty'
);
case PropertyDescriptionValidator::CODE_TOO_LONG:
$limit = $context[PropertyDescriptionValidator::CONTEXT_LIMIT];
throw new UseCaseError(
UseCaseError::DESCRIPTION_TOO_LONG,
"Description must be no more than $limit characters long",
[
UseCaseError::CONTEXT_VALUE => $context[PropertyDescriptionValidator::CONTEXT_DESCRIPTION],
UseCaseError::CONTEXT_CHARACTER_LIMIT => $limit,
]
);
case PropertyDescriptionValidator::CODE_LABEL_DESCRIPTION_EQUAL:
throw new UseCaseError(
UseCaseError::LABEL_DESCRIPTION_SAME_VALUE,
"Label and description for language code '$language' can not have the same value",
[ UseCaseError::CONTEXT_LANGUAGE => $context[PropertyDescriptionValidator::CONTEXT_LANGUAGE] ]
);
default:
throw new LogicException( "Unexpected validation error code: $errorCode" );
}
}

return new Term( $language, $request->getDescription() );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare( strict_types=1 );

namespace Wikibase\Repo\RestApi\Application\Validation;

use Wikibase\DataModel\Entity\PropertyId;

/**
* @license GPL-2.0-or-later
*/
interface PropertyDescriptionValidator {

public const CODE_INVALID = 'invalid-description';
public const CODE_EMPTY = 'description-empty';
public const CODE_TOO_LONG = 'description-too-long';
public const CODE_LABEL_DESCRIPTION_EQUAL = 'label-description-equal';

public const CONTEXT_LIMIT = 'character-limit';
public const CONTEXT_LANGUAGE = 'language';
public const CONTEXT_DESCRIPTION = 'description';

public function validate( PropertyId $propertyId, string $language, string $description ): ?ValidationError;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php declare( strict_types=1 );

namespace Wikibase\Repo\RestApi\Infrastructure;

use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\Repo\RestApi\Application\Validation\PropertyDescriptionValidator;
use Wikibase\Repo\RestApi\Application\Validation\ValidationError;
use Wikibase\Repo\RestApi\Domain\Services\PropertyRetriever;
use Wikibase\Repo\Validators\TermValidatorFactory;

/**
* @license GPL-2.0-or-later
*/
class WikibaseRepoPropertyDescriptionValidator implements PropertyDescriptionValidator {

private TermValidatorFactory $termValidatorFactory;
private PropertyRetriever $propertyRetriever;

public function __construct(
TermValidatorFactory $termValidatorFactory,
PropertyRetriever $propertyRetriever
) {
$this->termValidatorFactory = $termValidatorFactory;
$this->propertyRetriever = $propertyRetriever;
}

public function validate( PropertyId $propertyId, string $language, string $description ): ?ValidationError {
return $this->validateDescription( $description )
?? $this->validateProperty( $propertyId, $language, $description );
}

private function validateDescription( string $description ): ?ValidationError {
$result = $this->termValidatorFactory
->getDescriptionValidator()
->validate( $description );
if ( !$result->isValid() ) {
$error = $result->getErrors()[0];
switch ( $error->getCode() ) {
case 'description-too-short':
return new ValidationError( self::CODE_EMPTY );
case 'description-too-long':
return new ValidationError(
self::CODE_TOO_LONG,
[
self::CONTEXT_DESCRIPTION => $description,
self::CONTEXT_LIMIT => $error->getParameters()[0],
]
);
default:
return new ValidationError(
self::CODE_INVALID,
[ self::CONTEXT_DESCRIPTION => $description ]
);
}
}

return null;
}

private function validateProperty( PropertyId $propertyId, string $language, string $description ): ?ValidationError {
$property = $this->propertyRetriever->getProperty( $propertyId );

// skip if Property does not exist
if ( $property === null ) {
return null;
}

// skip if description is unchanged
if ( $property->getDescriptions()->hasTermForLanguage( $language ) &&
$property->getDescriptions()->getByLanguage( $language )->getText() === $description
) {
return null;
}

// skip if Property does not have a label
if ( !$property->getLabels()->hasTermForLanguage( $language ) ) {
return null;
}

$label = $property->getLabels()->getByLanguage( $language )->getText();
if ( $label === $description ) {
return new ValidationError(
self::CODE_LABEL_DESCRIPTION_EQUAL,
[ self::CONTEXT_LANGUAGE => $language ]
);
}

return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare( strict_types=1 );

namespace Wikibase\Repo\Tests\RestApi\Application\UseCaseRequestValidation;

use Generator;
use PHPUnit\Framework\TestCase;
use Wikibase\DataModel\Term\Term;
use Wikibase\Repo\RestApi\Application\UseCaseRequestValidation\PropertyDescriptionEditRequest;
use Wikibase\Repo\RestApi\Application\UseCaseRequestValidation\PropertyDescriptionEditRequestValidatingDeserializer;
use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError;
use Wikibase\Repo\RestApi\Application\Validation\PropertyDescriptionValidator;
use Wikibase\Repo\RestApi\Application\Validation\ValidationError;

/**
* @covers \Wikibase\Repo\RestApi\Application\UseCaseRequestValidation\PropertyDescriptionEditRequestValidatingDeserializer
*
* @group Wikibase
*
* @license GPL-2.0-or-later
*/
class PropertyDescriptionEditRequestValidatingDeserializerTest extends TestCase {

public function testGivenValidRequest_returnsDescription(): void {
$request = $this->createStub( PropertyDescriptionEditRequest::class );
$request->method( 'getPropertyId' )->willReturn( 'P123' );
$request->method( 'getLanguageCode' )->willReturn( 'en' );
$request->method( 'getDescription' )->willReturn( 'that class of which this subject is a particular example and member' );

$this->assertEquals(
new Term( 'en', 'that class of which this subject is a particular example and member' ),
( new PropertyDescriptionEditRequestValidatingDeserializer( $this->createStub( PropertyDescriptionValidator::class ) ) )
->validateAndDeserialize( $request )
);
}

/**
* @dataProvider invalidDescriptionProvider
*/
public function testWithInvalidDescription(
ValidationError $validationError,
string $expectedErrorCode,
string $expectedErrorMessage,
array $expectedContext = []
): void {
$request = $this->createStub( PropertyDescriptionEditRequest::class );
$request->method( 'getPropertyId' )->willReturn( 'P123' );
$request->method( 'getLanguageCode' )->willReturn( 'en' );
$request->method( 'getDescription' )->willReturn( 'my description' );

$propertyDescriptionValidator = $this->createStub( PropertyDescriptionValidator::class );
$propertyDescriptionValidator->method( 'validate' )->willReturn( $validationError );

try {
( new PropertyDescriptionEditRequestValidatingDeserializer( $propertyDescriptionValidator ) )
->validateAndDeserialize( $request );
$this->fail( 'this should not be reached' );
} catch ( UseCaseError $error ) {
$this->assertSame( $expectedErrorCode, $error->getErrorCode() );
$this->assertSame( $expectedErrorMessage, $error->getErrorMessage() );
$this->assertSame( $expectedContext, $error->getErrorContext() );
}
}

public static function invalidDescriptionProvider(): Generator {
yield 'description empty' => [
new ValidationError( PropertyDescriptionValidator::CODE_EMPTY ),
UseCaseError::DESCRIPTION_EMPTY,
'Description must not be empty',
];

$description = 'description that is too long...';
$limit = 40;
yield 'description too long' => [
new ValidationError(
PropertyDescriptionValidator::CODE_TOO_LONG,
[
PropertyDescriptionValidator::CONTEXT_DESCRIPTION => $description,
PropertyDescriptionValidator::CONTEXT_LIMIT => $limit,
]
),
UseCaseError::DESCRIPTION_TOO_LONG,
'Description must be no more than 40 characters long',
[
UseCaseError::CONTEXT_VALUE => $description,
UseCaseError::CONTEXT_CHARACTER_LIMIT => $limit,
],
];

$description = "tab characters \t not allowed";
yield 'invalid description' => [
new ValidationError(
PropertyDescriptionValidator::CODE_INVALID,
[ PropertyDescriptionValidator::CONTEXT_DESCRIPTION => $description ],
),
UseCaseError::INVALID_DESCRIPTION,
"Not a valid description: $description",
];

$language = 'en';
yield 'label and description are equal' => [
new ValidationError(
PropertyDescriptionValidator::CODE_LABEL_DESCRIPTION_EQUAL,
[ PropertyDescriptionValidator::CONTEXT_LANGUAGE => $language ],
),
UseCaseError::LABEL_DESCRIPTION_SAME_VALUE,
"Label and description for language code '$language' can not have the same value",
[ UseCaseError::CONTEXT_LANGUAGE => $language ],
];
}

}
Loading

0 comments on commit f220947

Please sign in to comment.