src/SDL/Reflection/Builder/Process/Compiler.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php
/**
 * This file is part of Railt package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
declare(strict_types=1);

namespace Railt\SDL\Reflection\Builder\Process;

use Phplrt\Ast\NodeInterface;
use Phplrt\Ast\RuleInterface;
use Railt\SDL\Contracts\Definitions\Definition;
use Railt\SDL\Contracts\Definitions\TypeDefinition;
use Railt\SDL\Contracts\Dependent\DependentDefinition;
use Railt\SDL\Contracts\Document;
use Railt\SDL\Contracts\Invocations\Invocable;
use Railt\SDL\Reflection\Builder\DocumentBuilder;
use Railt\SDL\Reflection\Validation\Base\ValidatorInterface;
use Railt\SDL\Reflection\Validation\Definitions;
use Railt\SDL\Reflection\Validation\Uniqueness;
use Railt\SDL\Schema\CompilerInterface;
use Railt\SDL\Schema\Configuration;

/**
 * Trait Compiler
 */
trait Compiler
{
    /**
     * @var int
     */
    protected $offset = 0;

    /**
     * @var NodeInterface
     */
    private $ast;

    /**
     * @var array|string[]
     */
    private $siblingActions = [];

    /**
     * @var bool
     */
    private $completed = false;

    /**
     * @return void
     */
    public function compile(): void
    {
        if ($this->completed === false) {
            $this->completed = true;

            foreach ($this->getAst()->getChildren() as $child) {
                if ($this->compileSiblings($child)) {
                    continue;
                }

                if ($this->onCompile($child)) {
                    continue;
                }
            }
        }
    }

    /**
     * @return NodeInterface|RuleInterface
     */
    public function getAst(): NodeInterface
    {
        return $this->ast;
    }

    /**
     * @param NodeInterface $child
     * @return bool
     */
    protected function compileSiblings(NodeInterface $child): bool
    {
        foreach ($this->siblingActions as $action) {
            if ($this->$action($child)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return Document|DocumentBuilder
     */
    public function getDocument(): Document
    {
        return $this->document;
    }

    /**
     * @return CompilerInterface|Configuration
     */
    public function getCompiler(): CompilerInterface
    {
        return $this->getDocument()->getCompiler();
    }

    /**
     * @param string $group
     * @return ValidatorInterface
     * @throws \OutOfBoundsException
     */
    public function getValidator(string $group = null): ValidatorInterface
    {
        return $this->getCompiler()->getValidator($group);
    }

    /**
     * @return array
     */
    public function __sleep(): array
    {
        $result = ['offset'];

        if (\method_exists(parent::class, '__sleep')) {
            return \array_merge(parent::__sleep(), $result);
        }

        return $result;
    }

    /**
     * @return void
     */
    public function __wakeup(): void
    {
        $this->completed = true;
    }

    /**
     * @return int
     */
    public function getDeclarationLine(): int
    {
        return $this->getDocument()->getFile()->getPosition($this->offset)->getLine();
    }

    /**
     * @return int
     */
    public function getDeclarationColumn(): int
    {
        return $this->getDocument()->getFile()->getPosition($this->offset)->getColumn();
    }

    /**
     * @param NodeInterface $ast
     * @param Document $document
     * @return void
     */
    protected function boot(NodeInterface $ast, Document $document): void
    {
        $this->ast = $ast;
        $this->document = $document;

        // Generate identifier if id does not initialized
        $this->getUniqueId();

        // Collect sibling methods
        foreach (\class_uses_recursive(static::class) as $sibling) {
            $method = 'compile' . \class_basename($sibling);

            if (\method_exists($sibling, $method)) {
                $this->siblingActions[] = $method;
            }
        }

        /**
         * Initialize the name of the type, if it is an independent
         * unique definition of the type of GraphQL.
         */
        if ($this instanceof Definition) {
            $this->resolveTypeName();
        }

        if ($this instanceof Compilable && $this instanceof Invocable) {
            $this->getDocument()->future($this);

            return;
        }

        /**
         * If the type is not initialized by the Document, but
         * is a child of the root, then the lazy assembly is not needed.
         *
         * In this case we run it forcibly, and then we check its state.
         */
        if ($this instanceof DependentDefinition) {
            $this->getCompiler()->getCallStack()->push($this);

            // Force compile dependent definition
            $this->compile();

            // Normalize types
            $this->getCompiler()->getTypeCoercion()->apply($this);

            // Verify type
            $this->getValidator(Definitions::class)->validate($this);

            $this->getCompiler()->getCallStack()->pop();
        }
    }

    /**
     * @param string $name
     * @param string $desc
     * @return void
     */
    private function resolveTypeName(string $name = 'Name', string $desc = 'Description'): void
    {
        foreach ($this->getAst()->getChildren() as $child) {
            switch ($child->getName()) {
                case $name:
                    $node = $child->getChild(0);
                    [$this->name, $this->offset] = [$node->getValue(), $node->getOffset()];
                    break;

                case $desc:
                    $this->description = $this->parseDescription($child);
                    break;
            }
        }
    }

    /**
     * @param NodeInterface|RuleInterface $ast
     * @return string
     */
    private function parseDescription(NodeInterface $ast): string
    {
        $description = $this->parseValue($ast->getChild(0),'String');

        $depth = $this->getMinimalDescriptionDepth($description);

        $result = \preg_replace_callback('/^[^\n]+$/imu', static function (array $payload) use ($depth) {
            return \substr($payload[0], $depth);
        }, $description);

        return \trim($result);
    }

    /**
     * @param string $description
     * @return int
     */
    private function getMinimalDescriptionDepth(string $description): int
    {
        $min = null;

        $lines = \explode("\n", $description);
        $lines = \array_filter($lines, '\\trim');

        foreach ($lines as $line) {
            $current = \strlen($line) - \strlen(\ltrim($line));

            $min = $min === null ? $current : \min($current, $min);
        }

        return $min ?? 0;
    }

    /**
     * @param NodeInterface $ast
     * @param string $type
     * @param array $path
     * @return array|float|int|null|string
     */
    protected function parseValue(NodeInterface $ast, string $type, array $path = [])
    {
        return $this->getDocument()->getValueBuilder()->parse($ast, $type, $path);
    }

    /**
     * @param NodeInterface $ast
     * @return bool
     */
    protected function onCompile(NodeInterface $ast): bool
    {
        return false;
    }

    /**
     * @param string $type
     * @return TypeDefinition
     */
    protected function load(string $type): TypeDefinition
    {
        return $this->getCompiler()->getDictionary()->get($type, $this);
    }

    /**
     * @param array|TypeDefinition|null $field
     * @param TypeDefinition $definition
     * @return TypeDefinition|array
     */
    protected function unique($field, TypeDefinition $definition)
    {
        $this->getValidator(Uniqueness::class)->validate($field, $definition);

        if (\is_array($field)) {
            $field[$definition->getName()] = $definition;

            return $field;
        }

        return $definition;
    }

    /**
     * @param string $keyword
     * @return int
     */
    private function offsetPrefixedBy(string $keyword): int
    {
        return $this->offset - \strlen($keyword) - 1;
    }
}