src/SDL/Reflection/Builder/Invocations/ValueBuilder.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\Invocations;

use Phplrt\Ast\LeafInterface;
use Phplrt\Ast\NodeInterface;
use Phplrt\Ast\RuleInterface;
use Railt\SDL\Contracts\Document;
use Railt\SDL\Contracts\Invocations\InputInvocation;
use Railt\SDL\Reflection\Builder\DocumentBuilder;

/**
 * Class ValueBuilder
 */
class ValueBuilder
{
    private const AST_ID_ARRAY = 'List';
    private const AST_ID_OBJECT = 'Object';

    private const TOKEN_NULL = 'T_NULL';
    private const TOKEN_NUMBER = 'T_NUMBER_VALUE';
    private const TOKEN_BOOL_TRUE = 'T_BOOL_TRUE';
    private const TOKEN_BOOL_FALSE = 'T_BOOL_FALSE';

    /**
     * @var Document|DocumentBuilder
     */
    private $document;

    /**
     * ValueBuilder constructor.
     * @param Document $document
     */
    public function __construct(Document $document)
    {
        $this->document = $document;
    }

    /**
     * @param NodeInterface|RuleInterface|LeafInterface $ast
     * @param string $type
     * @param array $path
     * @return array|float|int|null|string
     * @throws \OutOfBoundsException
     */
    public function parse(NodeInterface $ast, string $type, array $path = [])
    {
        switch ($ast->getName()) {
            case self::AST_ID_ARRAY:
                return $this->toArray($ast, $type, $path);

            case self::AST_ID_OBJECT:
                return $this->toObject($ast, $type, $path);
        }

        return $this->toScalar($ast);
    }

    /**
     * @param NodeInterface|RuleInterface $ast
     * @param string $type
     * @param array $path
     * @return array
     */
    private function toArray(NodeInterface $ast, string $type, array $path): array
    {
        $result = [];

        foreach ($ast->getChildren() as $child) {
            $result[] = $this->parse($child->getChild(0), $type, $path);
        }

        return $result;
    }

    /**
     * @param NodeInterface $ast
     * @param string $type
     * @param array $path
     * @return InputInvocation
     */
    private function toObject(NodeInterface $ast, string $type, array $path): InputInvocation
    {
        return new InputInvocationBuilder($ast, $this->document, $type, $path);
    }

    /**
     * @param LeafInterface $ast
     * @return float|int|string|null
     */
    private function toScalar(LeafInterface $ast)
    {
        switch ($ast->getName()) {
            case self::TOKEN_NUMBER:
                if (\strpos($ast->getValue(), '.') !== false) {
                    return $this->toFloat($ast);
                }

                return $this->toInt($ast);

            case self::TOKEN_NULL:
                return null;

            case self::TOKEN_BOOL_TRUE:
                return true;

            case self::TOKEN_BOOL_FALSE:
                return false;
        }

        return $this->toString($ast);
    }

    /**
     * @param LeafInterface $ast
     * @return float
     */
    private function toFloat(LeafInterface $ast): float
    {
        return (float)$ast->getValue();
    }

    /**
     * @param LeafInterface $ast
     * @return int
     */
    private function toInt(LeafInterface $ast): int
    {
        return (int)$ast->getValue();
    }

    /**
     * @param LeafInterface $ast
     * @return string
     */
    private function toString(LeafInterface $ast): string
    {
        $result = $this->unpackStringData($ast);

        // Transform utf char \uXXXX -> X
        $result = $this->renderUtfSequences($result);

        // Transform special chars
        $result = $this->renderSpecialCharacters($result);

        // Unescape slashes "Some\\Any" => "Some\Any"
        $result = \stripcslashes($result);

        return $result;
    }

    /**
     * @param LeafInterface $ast
     * @return string
     */
    private function unpackStringData(LeafInterface $ast): string
    {
        switch (true) {
            case $ast->getName() === 'T_STRING':
                return \substr($ast->getValue(), 1, -1);
            case $ast->getName() === 'T_MULTILINE_STRING':
                return \substr($ast->getValue(), 3, -3);
        }

        return (string)$ast->getValue();
    }

    /**
     * Method for parsing special control characters.
     *
     * @see http://facebook.github.io/graphql/October2016/#sec-String-Value
     *
     * @param string $body
     * @return string
     */
    private function renderSpecialCharacters(string $body): string
    {
        // TODO Probably may be escaped by backslash like "\\n".
        $source = ['\b', '\f', '\n', '\r', '\t'];
        $out = ["\u{0008}", "\u{000C}", "\u{000A}", "\u{000D}", "\u {0009}"];

        return \str_replace($source, $out, $body);
    }

    /**
     * Method for parsing and decode utf-8 character
     * sequences like "\uXXXX" type.
     *
     * @see http://facebook.github.io/graphql/October2016/#sec-String-Value
     * @param string $body
     * @return string
     */
    private function renderUtfSequences(string $body): string
    {
        // TODO Probably may be escaped by backslash like "\\u0000"
        $pattern = '/\\\\u([0-9a-fA-F]{4})/';

        $callee = function (array $matches): string {
            [$char, $code] = [$matches[0], $matches[1]];

            try {
                $rendered = \pack('H*', $code);

                return \mb_convert_encoding($rendered, 'UTF-8', 'UCS-2BE');
            } catch (\Error | \ErrorException $error) {
                // Fallback?
                return $char;
            }
        };

        return @\preg_replace_callback($pattern, $callee, $body) ?? $body;
    }
}