wol-soft/php-json-schema-model-generator

View on GitHub
src/PropertyProcessor/Property/ArrayProcessor.php

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
<?php

declare(strict_types = 1);

namespace PHPModelGenerator\PropertyProcessor\Property;

use PHPMicroTemplate\Exception\FileSystemException;
use PHPMicroTemplate\Exception\SyntaxErrorException;
use PHPMicroTemplate\Exception\UndefinedSymbolException;
use PHPModelGenerator\Exception\Arrays\AdditionalTupleItemsException;
use PHPModelGenerator\Exception\Arrays\ContainsException;
use PHPModelGenerator\Exception\Arrays\MaxItemsException;
use PHPModelGenerator\Exception\Arrays\MinItemsException;
use PHPModelGenerator\Exception\Arrays\UniqueItemsException;
use PHPModelGenerator\Exception\SchemaException;
use PHPModelGenerator\Model\Property\PropertyInterface;
use PHPModelGenerator\Model\Property\PropertyType;
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
use PHPModelGenerator\Model\Validator;
use PHPModelGenerator\Model\Validator\AdditionalItemsValidator;
use PHPModelGenerator\Model\Validator\ArrayItemValidator;
use PHPModelGenerator\Model\Validator\ArrayTupleValidator;
use PHPModelGenerator\Model\Validator\PropertyTemplateValidator;
use PHPModelGenerator\Model\Validator\PropertyValidator;
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
use PHPModelGenerator\PropertyProcessor\Decorator\Property\DefaultArrayToEmptyArrayDecorator;
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
use PHPModelGenerator\Utils\RenderHelper;

/**
 * Class ArrayProcessor
 *
 * @package PHPModelGenerator\PropertyProcessor\Property
 */
class ArrayProcessor extends AbstractTypedValueProcessor
{
    protected const TYPE = 'array';

    private const JSON_FIELD_MIN_ITEMS = 'minItems';
    private const JSON_FIELD_MAX_ITEMS = 'maxItems';
    private const JSON_FIELD_ITEMS     = 'items';
    private const JSON_FIELD_CONTAINS  = 'contains';

    /**
     * @throws FileSystemException
     * @throws SchemaException
     * @throws SyntaxErrorException
     * @throws UndefinedSymbolException
     */
    protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        parent::generateValidators($property, $propertySchema);

        $this->addLengthValidation($property, $propertySchema);
        $this->addUniqueItemsValidation($property, $propertySchema);
        $this->addItemsValidation($property, $propertySchema);
        $this->addContainsValidation($property, $propertySchema);

        if (!$property->isRequired() &&
            $this->schemaProcessor->getGeneratorConfiguration()->isDefaultArraysToEmptyArrayEnabled()
        ) {
            $property->addDecorator(new DefaultArrayToEmptyArrayDecorator());

            if ($property->getType()) {
                $property->setType(
                    $property->getType(),
                    new PropertyType($property->getType(true)->getName(), false),
                );
            }

            if (!$property->getDefaultValue()) {
                $property->setDefaultValue([]);
            }
        }
    }

    /**
     * Add the vaidation for the allowed amount of items in the array
     */
    private function addLengthValidation(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        $json = $propertySchema->getJson();

        if (isset($json[self::JSON_FIELD_MIN_ITEMS])) {
            $property->addValidator(
                new PropertyValidator(
                    $property,
                    $this->getTypeCheck() . "count(\$value) < {$json[self::JSON_FIELD_MIN_ITEMS]}",
                    MinItemsException::class,
                    [$json[self::JSON_FIELD_MIN_ITEMS]],
                )
            );
        }

        if (isset($json[self::JSON_FIELD_MAX_ITEMS])) {
            $property->addValidator(
                new PropertyValidator(
                    $property,
                    $this->getTypeCheck() . "count(\$value) > {$json[self::JSON_FIELD_MAX_ITEMS]}",
                    MaxItemsException::class,
                    [$json[self::JSON_FIELD_MAX_ITEMS]],
                )
            );
        }
    }

    /**
     * Add the validator to check if the items inside an array are unique
     */
    private function addUniqueItemsValidation(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        $json = $propertySchema->getJson();

        if (!isset($json['uniqueItems']) || $json['uniqueItems'] !== true) {
            return;
        }

        $property->addValidator(
            new PropertyTemplateValidator(
                $property,
                DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayUnique.phptpl',
                [],
                UniqueItemsException::class,
            )
        );
    }

    /**
     * Add the validator to check for constraints required for each item
     *
     * @throws FileSystemException
     * @throws SchemaException
     * @throws SyntaxErrorException
     * @throws UndefinedSymbolException
     */
    private function addItemsValidation(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        $json = $propertySchema->getJson();

        if (!isset($json[self::JSON_FIELD_ITEMS])) {
            return;
        }

        // check if the items require a tuple validation
        if (is_array($json[self::JSON_FIELD_ITEMS]) &&
            array_keys($json[self::JSON_FIELD_ITEMS]) === range(0, count($json[self::JSON_FIELD_ITEMS]) - 1)
        ) {
            $this->addTupleValidator($property, $propertySchema);

            return;
        }

        $property->addValidator(
            new ArrayItemValidator(
                $this->schemaProcessor,
                $this->schema,
                $propertySchema->withJson($json[self::JSON_FIELD_ITEMS]),
                $property,
            )
        );
    }

    /**
     * Add the validator to check a tuple validation for each item of the array
     *
     * @throws SchemaException
     * @throws FileSystemException
     * @throws SyntaxErrorException
     * @throws UndefinedSymbolException
     */
    private function addTupleValidator(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        $json = $propertySchema->getJson();

        if (isset($json['additionalItems']) && $json['additionalItems'] !== true) {
            $this->addAdditionalItemsValidator($property, $propertySchema);
        }

        $property->addValidator(
            new ArrayTupleValidator(
                $this->schemaProcessor,
                $this->schema,
                $propertySchema->withJson($json[self::JSON_FIELD_ITEMS]),
                $property->getName(),
            )
        );
    }

    /**
     * @throws FileSystemException
     * @throws SchemaException
     * @throws SyntaxErrorException
     * @throws UndefinedSymbolException
     */
    private function addAdditionalItemsValidator(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        $json = $propertySchema->getJson();

        if (!is_bool($json['additionalItems'])) {
            $property->addValidator(
                new AdditionalItemsValidator(
                    $this->schemaProcessor,
                    $this->schema,
                    $propertySchema,
                    $property->getName(),
                )
            );

            return;
        }

        $expectedAmount = count($json[self::JSON_FIELD_ITEMS]);

        $property->addValidator(
            new PropertyValidator(
                $property,
                '($amount = count($value)) > ' . $expectedAmount,
                AdditionalTupleItemsException::class,
                [$expectedAmount, '&$amount'],
            )
        );
    }

    /**
     * Add the validator to check for constraints required for at least one item
     *
     * @throws SchemaException
     */
    private function addContainsValidation(PropertyInterface $property, JsonSchema $propertySchema): void
    {
        if (!isset($propertySchema->getJson()[self::JSON_FIELD_CONTAINS])) {
            return;
        }

        $name = "item of array {$property->getName()}";
        // an item of the array behaves like a nested property to add item-level validation
        $nestedProperty = (new PropertyFactory(new PropertyProcessorFactory()))
            ->create(
                new PropertyMetaDataCollection([$name]),
                $this->schemaProcessor,
                $this->schema,
                $name,
                $propertySchema->withJson($propertySchema->getJson()[self::JSON_FIELD_CONTAINS]),
            );

        $nestedProperty->filterValidators(static fn(Validator $validator): bool =>
            !is_a($validator->getValidator(), RequiredPropertyValidator::class)
        );

        $property->addValidator(
            new PropertyTemplateValidator(
                $property,
                DIRECTORY_SEPARATOR . 'Validator' . DIRECTORY_SEPARATOR . 'ArrayContains.phptpl',
                [
                    'property' => $nestedProperty,
                    'schema' => $this->schema,
                    'viewHelper' => new RenderHelper($this->schemaProcessor->getGeneratorConfiguration()),
                    'generatorConfiguration' => $this->schemaProcessor->getGeneratorConfiguration(),
                ],
                ContainsException::class,
            )
        );
    }
}