wikimedia/mediawiki-extensions-Wikibase

View on GitHub
lib/packages/wikibase/data-model-serialization/src/Deserializers/SnakDeserializer.php

Summary

Maintainability
A
45 mins
Test Coverage
<?php

namespace Wikibase\DataModel\Deserializers;

use DataValues\DataValue;
use DataValues\Deserializers\DataValueDeserializer;
use DataValues\UnDeserializableValue;
use Deserializers\Deserializer;
use Deserializers\DispatchableDeserializer;
use Deserializers\Exceptions\DeserializationException;
use Deserializers\Exceptions\InvalidAttributeException;
use Deserializers\Exceptions\MissingAttributeException;
use Deserializers\Exceptions\MissingTypeException;
use Deserializers\Exceptions\UnsupportedTypeException;
use Exception;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\EntityIdParsingException;
use Wikibase\DataModel\Entity\PropertyId;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
use Wikibase\DataModel\Snak\PropertyNoValueSnak;
use Wikibase\DataModel\Snak\PropertySomeValueSnak;
use Wikibase\DataModel\Snak\PropertyValueSnak;
use Wikibase\DataModel\Snak\SnakObject;

/**
 * Package private
 *
 * @license GPL-2.0-or-later
 */
class SnakDeserializer implements DispatchableDeserializer {

    private Deserializer $dataValueDeserializer;
    private EntityIdParser $propertyIdParser;
    private PropertyDataTypeLookup $dataTypeLookup;
    private array $deserializerBuilders;
    private array $dataTypeToValueTypeMap;
    private SnakValueDeserializer $snakValueDeserializer;

    public function __construct(
        EntityIdParser $propertyIdParser,
        Deserializer $dataValueDeserializer,
        PropertyDataTypeLookup $dataTypeLookup,
        array $deserializerBuilders,
        array $dataTypeToValueTypeMap,
        SnakValueDeserializer $snakValueDeserializer
    ) {
        $this->dataValueDeserializer = $dataValueDeserializer;
        $this->propertyIdParser = $propertyIdParser;
        $this->dataTypeLookup = $dataTypeLookup;
        $this->deserializerBuilders = $deserializerBuilders;
        $this->dataTypeToValueTypeMap = $dataTypeToValueTypeMap;
        $this->snakValueDeserializer = $snakValueDeserializer;
    }

    /**
     * @see Deserializer::isDeserializerFor
     *
     * @param mixed $serialization
     *
     * @return bool
     */
    public function isDeserializerFor( $serialization ): bool {
        return is_array( $serialization )
            && $this->hasSnakType( $serialization )
            && $this->hasCorrectSnakType( $serialization );
    }

    private function hasSnakType( array $serialization ): bool {
        return array_key_exists( 'snaktype', $serialization );
    }

    private function hasCorrectSnakType( array $serialization ): bool {
        return in_array( $serialization['snaktype'], [ 'novalue', 'somevalue', 'value' ] );
    }

    /**
     * @see Deserializer::deserialize
     *
     * @param array $serialization
     *
     * @throws DeserializationException
     * @return PropertyNoValueSnak|PropertySomeValueSnak|PropertyValueSnak
     */
    public function deserialize( $serialization ) {
        $this->assertCanDeserialize( $serialization );
        $this->requireAttribute( $serialization, 'property' );

        return $this->getDeserialized( $serialization );
    }

    /**
     * @see SnakDeserializer::hasCorrectSnakType
     *
     * @param array $serialization
     *
     * @throws InvalidAttributeException
     * @return PropertyNoValueSnak|PropertySomeValueSnak|PropertyValueSnak
     */
    private function getDeserialized( array $serialization ): SnakObject {
        switch ( $serialization['snaktype'] ) {
            case 'novalue':
                return $this->newNoValueSnak( $serialization );
            case 'somevalue':
                return $this->newSomeValueSnak( $serialization );
            default:
                return $this->newValueSnak( $serialization );
        }
    }

    private function newNoValueSnak( array $serialization ): PropertyNoValueSnak {
        return new PropertyNoValueSnak( $this->deserializePropertyId( $serialization['property'] ) );
    }

    private function newSomeValueSnak( array $serialization ): PropertySomeValueSnak {
        return new PropertySomeValueSnak( $this->deserializePropertyId( $serialization['property'] ) );
    }

    private function newValueSnak( array $serialization ): PropertyValueSnak {
        $this->requireAttribute( $serialization, 'datavalue' );
        $propertyId = $this->deserializePropertyId( $serialization['property'] );

        $this->assertValidDataValue( $serialization['datavalue'] );

        return new PropertyValueSnak(
            $propertyId,
            $this->deserializeDataValue( $propertyId, $serialization['datavalue'] )
        );
    }

    private function deserializeDataValue( PropertyId $propertyId, array $serialization ): DataValue {
        try {
            return $this->needsDataTypeLookup( $serialization[DataValueDeserializer::TYPE_KEY] )
                ? $this->lookUpDataTypeAndDeserializeValue( $propertyId, $serialization )
                : $this->dataValueDeserializer->deserialize( $serialization );
        } catch ( DeserializationException $ex ) {
            return $this->newUndeserializableValue( $serialization, $ex );
        }
    }

    private function assertValidDataValue( $serialization ): void {
        if ( !is_array( $serialization ) || !array_key_exists( DataValueDeserializer::TYPE_KEY, $serialization ) ) {
            throw new MissingTypeException( 'Not an array or missing the key "' . DataValueDeserializer::TYPE_KEY . '"' );
        }

        if ( !array_key_exists( DataValueDeserializer::VALUE_KEY, $serialization ) ) {
            throw new MissingAttributeException( DataValueDeserializer::VALUE_KEY );
        }
    }

    /**
     * We only need to look up the data type if the value type needs a data type specific parser.
     */
    private function needsDataTypeLookup( string $valueType ): bool {
        $possibleDataTypeKeys = array_map(
            fn( string $dataType ) => "PT:$dataType",
            array_keys( $this->dataTypeToValueTypeMap, $valueType, true )
        );

        return !empty( array_intersect( $possibleDataTypeKeys, array_keys( $this->deserializerBuilders ) ) );
    }

    /**
     * @throws InvalidAttributeException
     */
    private function deserializePropertyId( string $serialization ): PropertyId {
        try {
            $id = $this->propertyIdParser->parse( $serialization );
        } catch ( EntityIdParsingException $ex ) {
            throw new InvalidAttributeException(
                'property',
                $serialization,
                "'$serialization' is not a valid property ID"
            );
        }

        if ( $id instanceof PropertyId ) {
            return $id;
        }

        throw new InvalidAttributeException(
            'property',
            $serialization,
            "'$serialization' is not a property ID"
        );
    }

    private function assertCanDeserialize( $serialization ): void {
        if ( !is_array( $serialization ) ) {
            throw new DeserializationException( 'The snak serialization should be an array' );
        }

        if ( !$this->hasSnakType( $serialization ) ) {
            throw new MissingTypeException();
        }

        if ( !$this->hasCorrectSnakType( $serialization ) ) {
            throw new UnsupportedTypeException( $serialization['snaktype'] );
        }
    }

    private function requireAttribute( array $array, string $attributeName ): void {
        if ( !array_key_exists( $attributeName, $array ) ) {
            throw new MissingAttributeException(
                $attributeName
            );
        }
    }

    private function newUndeserializableValue( array $serialization, Exception $exception ): UnDeserializableValue {
        $value = $serialization[DataValueDeserializer::VALUE_KEY] ?? null;
        $type = $serialization[DataValueDeserializer::TYPE_KEY] ?? null;
        $error = $serialization['error'] ?? $exception->getMessage();

        return new UnDeserializableValue( $value, $type, $error );
    }

    private function lookUpDataTypeAndDeserializeValue( PropertyId $propertyId, array $serialization ): DataValue {
        try {
            $dataType = $this->dataTypeLookup->getDataTypeIdForProperty( $propertyId );
        } catch ( Exception $e ) {
            return $this->newUndeserializableValue( $serialization, $e );
        }

        return $this->snakValueDeserializer->deserialize( $dataType, $serialization );
    }

}