SoftCreatR/JSONPath

View on GitHub
src/JSONPathLexer.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

/**
 * JSONPath implementation for PHP.
 *
 * @license https://github.com/SoftCreatR/JSONPath/blob/main/LICENSE  MIT License
 */

namespace Flow\JSONPath;

class JSONPathLexer
{
    /*
     * Match within bracket groups
     * Matches are whitespace insensitive
     */
    public const MATCH_INDEX = '(?!-)[\-\w]+ | \*'; // e.g.: foo or 40f35757-2563-4790-b0b1-caa904be455f

    public const MATCH_INDEXES = '\s* -?\d+ [-?\d,\s]+'; // Eg. 0,1,2

    public const MATCH_SLICE = '[-\d:]+ | :'; // Eg. [0:2:1]

    public const MATCH_QUERY_RESULT = '\s* \( .+? \) \s*'; // Eg. ?(@.length - 1)

    public const MATCH_QUERY_MATCH = '\s* \?\(.+?\) \s*'; // Eg. ?(@.foo = "bar")

    public const MATCH_INDEX_IN_SINGLE_QUOTES = '\s* \' (.+?)? \' \s*'; // Eg. 'bar'

    public const MATCH_INDEX_IN_DOUBLE_QUOTES = '\s* " (.+?)? " \s*'; // Eg. "bar"

    /**
     * The expression being lexed.
     */
    protected string $expression = '';

    /**
     * The length of the expression.
     */
    protected int $expressionLength = 0;

    public function __construct(string $expression)
    {
        $expression = \trim($expression);
        $len = \strlen($expression);

        if ($len > 1) {
            if ($expression[0] === '$') {
                $expression = \substr($expression, 1);
                $len--;
            }

            if ($expression[0] !== '.' && $expression[0] !== '[') {
                $expression = '.' . $expression;
                $len++;
            }

            $this->expression = $expression;
            $this->expressionLength = $len;
        }
    }

    /**
     * @throws JSONPathException
     */
    public function parseExpressionTokens(): array
    {
        $dotIndexDepth = 0;
        $squareBracketDepth = 0;
        $tokenValue = '';
        $tokens = [];

        for ($i = 0; $i < $this->expressionLength; $i++) {
            $char = $this->expression[$i];

            if (($squareBracketDepth === 0) && $char === '.') {
                if ($this->lookAhead($i) === '.') {
                    $tokens[] = new JSONPathToken(JSONPathToken::T_RECURSIVE, null);
                }

                continue;
            }

            if ($char === '[') {
                $squareBracketDepth++;

                if ($squareBracketDepth === 1) {
                    continue;
                }
            }

            if ($char === ']') {
                $squareBracketDepth--;

                if ($squareBracketDepth === 0) {
                    continue;
                }
            }

            /*
             * Within square brackets
             */
            if ($squareBracketDepth > 0) {
                $tokenValue .= $char;

                if ($squareBracketDepth === 1 && $this->lookAhead($i) === ']') {
                    $tokens[] = $this->createToken($tokenValue);
                    $tokenValue = '';
                }
            }

            /*
             * Outside square brackets
             */
            if ($squareBracketDepth === 0) {
                $tokenValue .= $char;

                // Double dot ".."
                if ($char === '.' && $dotIndexDepth > 1) {
                    $tokens[] = $this->createToken($tokenValue);
                    $tokenValue = '';
                    continue;
                }

                if ($this->atEnd($i) || \in_array($this->lookAhead($i), ['.', '['])) {
                    $tokens[] = $this->createToken($tokenValue);
                    $tokenValue = '';
                    $dotIndexDepth--;
                }
            }
        }

        if ($tokenValue !== '') {
            $tokens[] = $this->createToken($tokenValue);
        }

        return $tokens;
    }

    protected function lookAhead(int $pos, int $forward = 1): ?string
    {
        return $this->expression[$pos + $forward] ?? null;
    }

    protected function atEnd(int $pos): bool
    {
        return $pos === $this->expressionLength;
    }

    /**
     * @throws JSONPathException
     */
    public function parseExpression(): array
    {
        return $this->parseExpressionTokens();
    }

    /**
     * @throws JSONPathException
     */
    protected function createToken(string $value): JSONPathToken
    {
        // The IDE doesn't like, what we do with $value, so let's
        // move it to a separate variable, to get rid of any IDE warnings
        $tokenValue = $value;

        /** @var JSONPathToken|null $ret */
        $ret = null;

        if (\preg_match('/^(' . static::MATCH_INDEX . ')$/xu', $tokenValue, $matches)) {
            if (\preg_match('/^-?\d+$/', $tokenValue)) {
                $tokenValue = (int)$tokenValue;
            }

            $ret = new JSONPathToken(JSONPathToken::T_INDEX, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_INDEXES . '$/xu', $tokenValue, $matches)) {
            $tokenValue = \explode(',', \trim($tokenValue, ','));

            foreach ($tokenValue as $i => $v) {
                $tokenValue[$i] = (int)\trim($v);
            }

            $ret = new JSONPathToken(JSONPathToken::T_INDEXES, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_SLICE . '$/xu', $tokenValue, $matches)) {
            $parts = \explode(':', $tokenValue);
            $tokenValue = [
                'start' => isset($parts[0]) && $parts[0] !== '' ? (int)$parts[0] : null,
                'end' => isset($parts[1]) && $parts[1] !== '' ? (int)$parts[1] : null,
                'step' => isset($parts[2]) && $parts[2] !== '' ? (int)$parts[2] : null,
            ];

            $ret = new JSONPathToken(JSONPathToken::T_SLICE, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_QUERY_RESULT . '$/xu', $tokenValue)) {
            $tokenValue = \substr($tokenValue, 1, -1);

            $ret = new JSONPathToken(JSONPathToken::T_QUERY_RESULT, $tokenValue);
        } elseif (\preg_match('/^' . static::MATCH_QUERY_MATCH . '$/xu', $tokenValue)) {
            $tokenValue = \substr($tokenValue, 2, -1);

            $ret = new JSONPathToken(JSONPathToken::T_QUERY_MATCH, $tokenValue);
        } elseif (
            \preg_match('/^' . static::MATCH_INDEX_IN_SINGLE_QUOTES . '$/xu', $tokenValue, $matches)
            || \preg_match('/^' . static::MATCH_INDEX_IN_DOUBLE_QUOTES . '$/xu', $tokenValue, $matches)
        ) {
            if (isset($matches[1])) {
                $tokenValue = $matches[1];
                $tokenValue = \trim($tokenValue);

                $possibleArray = false;
                if ($matches[0][0] === '"') {
                    $possibleArray = \explode('","', $tokenValue);
                } elseif ($matches[0][0] === "'") {
                    $possibleArray = \explode("','", $tokenValue);
                }
                if ($possibleArray !== false && \count($possibleArray) > 1) {
                    $tokenValue = $possibleArray;
                }
            } else {
                $tokenValue = '';
            }

            $ret = new JSONPathToken(JSONPathToken::T_INDEX, $tokenValue);
        }

        if ($ret !== null) {
            return $ret;
        }

        throw new JSONPathException("Unable to parse token {$tokenValue} in expression: {$this->expression}");
    }
}