wikimedia/mediawiki-extensions-Wikibase

View on GitHub
repo/includes/Validators/SnakValidator.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Wikibase\Repo\Validators;

use DataValues\DataValue;
use DataValues\UnDeserializableValue;
use InvalidArgumentException;
use OutOfBoundsException;
use ValueValidators\Error;
use ValueValidators\Result;
use ValueValidators\ValueValidator;
use Wikibase\DataModel\Reference;
use Wikibase\DataModel\ReferenceList;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookupException;
use Wikibase\DataModel\Snak\PropertyValueSnak;
use Wikibase\DataModel\Snak\Snak;
use Wikibase\DataModel\Statement\Statement;
use Wikibase\Lib\DataTypeFactory;
use Wikibase\Repo\DataTypeValidatorFactory;

/**
 * @license GPL-2.0-or-later
 * @author Daniel Kinzler
 */
class SnakValidator implements ValueValidator {

    /**
     * @var DataTypeFactory
     */
    private $dataTypeFactory;

    /**
     * @var PropertyDataTypeLookup
     */
    private $propertyDataTypeLookup;

    /**
     * @var DataTypeValidatorFactory
     */
    private $validatorFactory;

    public function __construct(
        PropertyDataTypeLookup $propertyDataTypeLookup,
        DataTypeFactory $dataTypeFactory,
        DataTypeValidatorFactory $validatorFactory
    ) {
        $this->dataTypeFactory = $dataTypeFactory;
        $this->propertyDataTypeLookup = $propertyDataTypeLookup;
        $this->validatorFactory = $validatorFactory;
    }

    /**
     * Validates the Statement's main snak, qualifiers, and references.
     */
    public function validateStatementSnaks( Statement $statement ): Result {
        $snak = $statement->getMainSnak();
        $result = $this->validate( $snak );

        if ( !$result->isValid() ) {
            return $result;
        }

        foreach ( $statement->getQualifiers() as $snak ) {
            $result = $this->validate( $snak );

            if ( !$result->isValid() ) {
                return $result;
            }
        }

        $result = $this->validateReferences( $statement->getReferences() );

        if ( !$result->isValid() ) {
            return $result;
        }

        return Result::newSuccess();
    }

    /**
     * Validate a list of references.
     * This is done by validating all snaks in all of the references.
     *
     * @param ReferenceList $references
     *
     * @return Result
     */
    public function validateReferences( ReferenceList $references ) {
        foreach ( $references as $reference ) {
            $result = $this->validateReference( $reference );

            if ( !$result->isValid() ) {
                return $result;
            }
        }

        return Result::newSuccess();
    }

    /**
     * Validate a list of references.
     * This is done by validating all snaks in all of the references.
     *
     * @param Reference $reference
     *
     * @return Result
     */
    public function validateReference( Reference $reference ) {
        foreach ( $reference->getSnaks() as $snak ) {
            $result = $this->validate( $snak );

            if ( !$result->isValid() ) {
                return $result;
            }
        }

        return Result::newSuccess();
    }

    /**
     * Validates a Snak.
     * For a PropertyValueSnak, this is done using the validators from the DataType
     * that is associated with the Snak's property.
     * Other Snak types are currently not validated.
     *
     * @see ValueValidator::validate()
     *
     * @param Snak $snak The value to validate
     *
     * @throws InvalidArgumentException
     * @return Result
     */
    public function validate( $snak ) {
        if ( !( $snak instanceof Snak ) ) {
            throw new InvalidArgumentException( 'Snak expected' );
        }

        // XXX: instead of an instanceof check, we could have multiple validators
        //      with a canValidate() method, to determine which validator to use
        //      for a given snak.

        $propertyId = $snak->getPropertyId();

        try {
            $typeId = $this->propertyDataTypeLookup->getDataTypeIdForProperty( $propertyId );

            if ( $snak instanceof PropertyValueSnak ) {
                $dataValue = $snak->getDataValue();
                $result = $this->validateDataValue( $dataValue, $typeId );
            } else {
                $result = Result::newSuccess();
            }
        } catch ( PropertyDataTypeLookupException $ex ) {
            $result = Result::newError( [
                Error::newError( "Property $propertyId not found!", null, 'no-such-property', [ $propertyId ] ),
            ] );
        }

        return $result;
    }

    /**
     * Validates the given data value using the given data type.
     *
     * @param DataValue $dataValue
     * @param string $dataTypeId
     *
     * @return Result
     */
    public function validateDataValue( DataValue $dataValue, $dataTypeId ) {
        try {
            $dataValueType = $this->dataTypeFactory->getType( $dataTypeId )->getDataValueType();
        } catch ( OutOfBoundsException $ex ) {
            return Result::newError( [
                Error::newError(
                    'Bad data type: ' . $dataTypeId,
                    null,
                    'bad-data-type',
                    [ $dataTypeId ]
                ),
            ] );
        }

        if ( $dataValue instanceof UnDeserializableValue ) {
            return Result::newError( [
                Error::newError(
                    'Bad snak value: ' . $dataValue->getReason(),
                    null,
                    'bad-value',
                    [ $dataValue->getReason() ]
                ),
            ] );
        } elseif ( $dataValueType != $dataValue->getType() ) {
            return Result::newError( [
                Error::newError(
                    'Bad value type: ' . $dataValue->getType() . ', expected ' . $dataValueType,
                    null,
                    'bad-value-type',
                    [ $dataValue->getType(), $dataValueType ]
                ),
            ] );
        }

        $result = Result::newSuccess();

        //XXX: DataTypeValidatorFactory should expose only one validator, which would be a CompositeValidator
        foreach ( $this->validatorFactory->getValidators( $dataTypeId ) as $validator ) {
            $subResult = $validator->validate( $dataValue );

            //XXX: Some validators should be fatal and cause us to abort the loop.
            //     Others shouldn't.

            if ( !$subResult->isValid() ) {
                //TODO: Don't bail out immediately. Accumulate errors from all validators.
                //      We need Result::merge() for this.
                return $subResult;
            }
        }

        return $result;
    }

    /**
     * @see ValueValidator::setOptions()
     *
     * @param array $options
     *
     * @codeCoverageIgnore
     */
    public function setOptions( array $options ) {
        // Do nothing. This method shouldn't even be in the interface.
    }

}