diff options
Diffstat (limited to 'vendor/mtdowling/jmespath.php')
18 files changed, 3050 insertions, 0 deletions
diff --git a/vendor/mtdowling/jmespath.php/CHANGELOG.md b/vendor/mtdowling/jmespath.php/CHANGELOG.md new file mode 100644 index 0000000..d97dffb --- /dev/null +++ b/vendor/mtdowling/jmespath.php/CHANGELOG.md @@ -0,0 +1,62 @@ +# CHANGELOG + +## 2.6.0 - 2020-07-31 + +* Support for PHP 8.0. + +## 2.5.0 - 2019-12-30 + +* Full support for PHP 7.0-7.4. +* Fixed autoloading when run from within vendor folder. +* Full multibyte (UTF-8) string support. + +## 2.4.0 - 2016-12-03 + +* Added support for floats when interpreting data. +* Added a function_exists check to work around redeclaration issues. + +## 2.3.0 - 2016-01-05 + +* Added support for [JEP-9](https://github.com/jmespath/jmespath.site/blob/master/docs/proposals/improved-filters.rst), + including unary filter expressions, and `&&` filter expressions. +* Fixed various parsing issues, including not removing escaped single quotes + from raw string literals. +* Added support for the `map` function. +* Fixed several issues with code generation. + +## 2.2.0 - 2015-05-27 + +* Added support for [JEP-12](https://github.com/jmespath/jmespath.site/blob/master/docs/proposals/raw-string-literals.rst) + and raw string literals (e.g., `'foo'`). + +## 2.1.0 - 2014-01-13 + +* Added `JmesPath\Env::cleanCompileDir()` to delete any previously compiled + JMESPath expressions. + +## 2.0.0 - 2014-01-11 + +* Moving to a flattened namespace structure. +* Runtimes are now only PHP callables. +* Fixed an error in the way empty JSON literals are parsed so that they now + return an empty string to match the Python and JavaScript implementations. +* Removed functions from runtimes. Instead there is now a function dispatcher + class, FnDispatcher, that provides function implementations behind a single + dispatch function. +* Removed ExprNode in lieu of just using a PHP callable with bound variables. +* Removed debug methods from runtimes and instead into a new Debugger class. +* Heavily cleaned up function argument validation. +* Slice syntax is now properly validated (i.e., colons are followed by the + appropriate value). +* Lots of code cleanup and performance improvements. +* Added a convenient `JmesPath\search()` function. +* **IMPORTANT**: Relocating the project to https://github.com/jmespath/jmespath.php + +## 1.1.1 - 2014-10-08 + +* Added support for using ArrayAccess and Countable as arrays and objects. + +## 1.1.0 - 2014-08-06 + +* Added the ability to search data returned from json_decode() where JSON + objects are returned as stdClass objects. diff --git a/vendor/mtdowling/jmespath.php/LICENSE b/vendor/mtdowling/jmespath.php/LICENSE new file mode 100644 index 0000000..5c970a4 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/mtdowling/jmespath.php/README.rst b/vendor/mtdowling/jmespath.php/README.rst new file mode 100644 index 0000000..b65ee46 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/README.rst @@ -0,0 +1,123 @@ +============ +jmespath.php +============ + +JMESPath (pronounced "jaymz path") allows you to declaratively specify how to +extract elements from a JSON document. *jmespath.php* allows you to use +JMESPath in PHP applications with PHP data structures. It requires PHP 5.4 or +greater and can be installed through `Composer <http://getcomposer.org/doc/00-intro.md>`_ +using the ``mtdowling/jmespath.php`` package. + +.. code-block:: php + + require 'vendor/autoload.php'; + + $expression = 'foo.*.baz'; + + $data = [ + 'foo' => [ + 'bar' => ['baz' => 1], + 'bam' => ['baz' => 2], + 'boo' => ['baz' => 3] + ] + ]; + + JmesPath\search($expression, $data); + // Returns: [1, 2, 3] + +- `JMESPath Tutorial <http://jmespath.org/tutorial.html>`_ +- `JMESPath Grammar <http://jmespath.org/specification.html#grammar>`_ +- `JMESPath Python library <https://github.com/jmespath/jmespath.py>`_ + +PHP Usage +========= + +The ``JmesPath\search`` function can be used in most cases when using the +library. This function utilizes a JMESPath runtime based on your environment. +The runtime utilized can be configured using environment variables and may at +some point in the future automatically utilize a C extension if available. + +.. code-block:: php + + $result = JmesPath\search($expression, $data); + + // or, if you require PSR-4 compliance. + $result = JmesPath\Env::search($expression, $data); + +Runtimes +-------- + +jmespath.php utilizes *runtimes*. There are currently two runtimes: +AstRuntime and CompilerRuntime. + +AstRuntime is utilized by ``JmesPath\search()`` and ``JmesPath\Env::search()`` +by default. + +AstRuntime +~~~~~~~~~~ + +The AstRuntime will parse an expression, cache the resulting AST in memory, +and interpret the AST using an external tree visitor. AstRuntime provides a +good general approach for interpreting JMESPath expressions that have a low to +moderate level of reuse. + +.. code-block:: php + + $runtime = new JmesPath\AstRuntime(); + $runtime('foo.bar', ['foo' => ['bar' => 'baz']]); + // > 'baz' + +CompilerRuntime +~~~~~~~~~~~~~~~ + +``JmesPath\CompilerRuntime`` provides the most performance for +applications that have a moderate to high level of reuse of JMESPath +expressions. The CompilerRuntime will walk a JMESPath AST and emit PHP source +code, resulting in anywhere from 7x to 60x speed improvements. + +Compiling JMESPath expressions to source code is a slower process than just +walking and interpreting a JMESPath AST (via the AstRuntime). However, +running the compiled JMESPath code results in much better performance than +walking an AST. This essentially means that there is a warm-up period when +using the ``CompilerRuntime``, but after the warm-up period, it will provide +much better performance. + +Use the CompilerRuntime if you know that you will be executing JMESPath +expressions more than once or if you can pre-compile JMESPath expressions +before executing them (for example, server-side applications). + +.. code-block:: php + + // Note: The cache directory argument is optional. + $runtime = new JmesPath\CompilerRuntime('/path/to/compile/folder'); + $runtime('foo.bar', ['foo' => ['bar' => 'baz']]); + // > 'baz' + +Environment Variables +^^^^^^^^^^^^^^^^^^^^^ + +You can utilize the CompilerRuntime in ``JmesPath\search()`` by setting +the ``JP_PHP_COMPILE`` environment variable to "on" or to a directory +on disk used to store cached expressions. + +Testing +======= + +A comprehensive list of test cases can be found at +https://github.com/jmespath/jmespath.php/tree/master/tests/compliance. +These compliance tests are utilized by jmespath.php to ensure consistency with +other implementations, and can serve as examples of the language. + +jmespath.php is tested using PHPUnit. In order to run the tests, you need to +first install the dependencies using Composer as described in the *Installation* +section. Next you just need to run the tests via make: + +.. code-block:: bash + + make test + +You can run a suite of performance tests as well: + +.. code-block:: bash + + make perf diff --git a/vendor/mtdowling/jmespath.php/bin/jp.php b/vendor/mtdowling/jmespath.php/bin/jp.php new file mode 100755 index 0000000..c8433b5 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/bin/jp.php @@ -0,0 +1,74 @@ +#!/usr/bin/env php +<?php + +if (file_exists(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../../../autoload.php')) { + require __DIR__ . '/../../../autoload.php'; +} elseif (file_exists(__DIR__ . '/../autoload.php')) { + require __DIR__ . '/../autoload.php'; +} else { + throw new RuntimeException('Unable to locate autoload.php file.'); +} + +use JmesPath\Env; +use JmesPath\DebugRuntime; + +$description = <<<EOT +Runs a JMESPath expression on the provided input or a test case. + +Provide the JSON input and expression: + echo '{}' | jp.php expression + +Or provide the path to a compliance script, a suite, and test case number: + jp.php --script path_to_script --suite test_suite_number --case test_case_number [expression] + +EOT; + +$args = []; +$currentKey = null; +for ($i = 1, $total = count($argv); $i < $total; $i++) { + if ($i % 2) { + if (substr($argv[$i], 0, 2) == '--') { + $currentKey = str_replace('--', '', $argv[$i]); + } else { + $currentKey = trim($argv[$i]); + } + } else { + $args[$currentKey] = $argv[$i]; + $currentKey = null; + } +} + +$expression = $currentKey; + +if (isset($args['file']) || isset($args['suite']) || isset($args['case'])) { + if (!isset($args['file']) || !isset($args['suite']) || !isset($args['case'])) { + die($description); + } + // Manually run a compliance test + $path = realpath($args['file']); + file_exists($path) or die('File not found at ' . $path); + $json = json_decode(file_get_contents($path), true); + $set = $json[$args['suite']]; + $data = $set['given']; + if (!isset($expression)) { + $expression = $set['cases'][$args['case']]['expression']; + echo "Expects\n=======\n"; + if (isset($set['cases'][$args['case']]['result'])) { + echo json_encode($set['cases'][$args['case']]['result'], JSON_PRETTY_PRINT) . "\n\n"; + } elseif (isset($set['cases'][$args['case']]['error'])) { + echo "{$set['cases'][$argv['case']]['error']} error\n\n"; + } else { + echo "NULL\n\n"; + } + } +} elseif (isset($expression)) { + // Pass in an expression and STDIN as a standalone argument + $data = json_decode(stream_get_contents(STDIN), true); +} else { + die($description); +} + +$runtime = new DebugRuntime(Env::createRuntime()); +$runtime($expression, $data); diff --git a/vendor/mtdowling/jmespath.php/bin/perf.php b/vendor/mtdowling/jmespath.php/bin/perf.php new file mode 100755 index 0000000..aa93959 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/bin/perf.php @@ -0,0 +1,68 @@ +#!/usr/bin/env php +<?php + +if (file_exists(__DIR__ . '/../vendor/autoload.php')) { + require __DIR__ . '/../vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../../../autoload.php')) { + require __DIR__ . '/../../../autoload.php'; +} else { + throw new RuntimeException('Unable to locate autoload.php file.'); +} + +$xdebug = new \Composer\XdebugHandler\XdebugHandler('perf.php'); +$xdebug->check(); +unset($xdebug); + +$dir = isset($argv[1]) ? $argv[1] : __DIR__ . '/../tests/compliance/perf'; +is_dir($dir) or die('Dir not found: ' . $dir); +// Warm up the runner +\JmesPath\Env::search('foo', []); + +$total = 0; +foreach (glob($dir . '/*.json') as $file) { + $total += runSuite($file); +} +echo "\nTotal time: {$total}\n"; + +function runSuite($file) +{ + $contents = file_get_contents($file); + $json = json_decode($contents, true); + $total = 0; + foreach ($json as $suite) { + foreach ($suite['cases'] as $case) { + $total += runCase( + $suite['given'], + $case['expression'], + $case['name'] + ); + } + } + return $total; +} + +function runCase($given, $expression, $name) +{ + $best = 99999; + $runtime = \JmesPath\Env::createRuntime(); + + for ($i = 0; $i < 100; $i++) { + $t = microtime(true); + $runtime($expression, $given); + $tryTime = (microtime(true) - $t) * 1000; + if ($tryTime < $best) { + $best = $tryTime; + } + if (!getenv('CACHE')) { + $runtime = \JmesPath\Env::createRuntime(); + // Delete compiled scripts if not caching. + if ($runtime instanceof \JmesPath\CompilerRuntime) { + array_map('unlink', glob(sys_get_temp_dir() . '/jmespath_*.php')); + } + } + } + + printf("time: %07.4fms name: %s\n", $best, $name); + + return $best; +} diff --git a/vendor/mtdowling/jmespath.php/composer.json b/vendor/mtdowling/jmespath.php/composer.json new file mode 100644 index 0000000..6b70068 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/composer.json @@ -0,0 +1,39 @@ +{ + "name": "mtdowling/jmespath.php", + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": ["json", "jsonpath"], + "license": "MIT", + + "authors": [ + { + "name": "Michael Dowling", + "email": "[email protected]", + "homepage": "https://github.com/mtdowling" + } + ], + + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + + "require-dev": { + "composer/xdebug-handler": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": ["src/JmesPath.php"] + }, + + "bin": ["bin/jp.php"], + + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + } +} diff --git a/vendor/mtdowling/jmespath.php/src/AstRuntime.php b/vendor/mtdowling/jmespath.php/src/AstRuntime.php new file mode 100644 index 0000000..03f5f1a --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/AstRuntime.php @@ -0,0 +1,47 @@ +<?php +namespace JmesPath; + +/** + * Uses an external tree visitor to interpret an AST. + */ +class AstRuntime +{ + private $parser; + private $interpreter; + private $cache = []; + private $cachedCount = 0; + + public function __construct( + Parser $parser = null, + callable $fnDispatcher = null + ) { + $fnDispatcher = $fnDispatcher ?: FnDispatcher::getInstance(); + $this->interpreter = new TreeInterpreter($fnDispatcher); + $this->parser = $parser ?: new Parser(); + } + + /** + * Returns data from the provided input that matches a given JMESPath + * expression. + * + * @param string $expression JMESPath expression to evaluate + * @param mixed $data Data to search. This data should be data that + * is similar to data returned from json_decode + * using associative arrays rather than objects. + * + * @return mixed Returns the matching data or null + */ + public function __invoke($expression, $data) + { + if (!isset($this->cache[$expression])) { + // Clear the AST cache when it hits 1024 entries + if (++$this->cachedCount > 1024) { + $this->cache = []; + $this->cachedCount = 0; + } + $this->cache[$expression] = $this->parser->parse($expression); + } + + return $this->interpreter->visit($this->cache[$expression], $data); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/CompilerRuntime.php b/vendor/mtdowling/jmespath.php/src/CompilerRuntime.php new file mode 100644 index 0000000..c26b09c --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/CompilerRuntime.php @@ -0,0 +1,83 @@ +<?php +namespace JmesPath; + +/** + * Compiles JMESPath expressions to PHP source code and executes it. + * + * JMESPath file names are stored in the cache directory using the following + * logic to determine the filename: + * + * 1. Start with the string "jmespath_" + * 2. Append the MD5 checksum of the expression. + * 3. Append ".php" + */ +class CompilerRuntime +{ + private $parser; + private $compiler; + private $cacheDir; + private $interpreter; + + /** + * @param string|null $dir Directory used to store compiled PHP files. + * @param Parser|null $parser JMESPath parser to utilize + * @throws \RuntimeException if the cache directory cannot be created + */ + public function __construct($dir = null, Parser $parser = null) + { + $this->parser = $parser ?: new Parser(); + $this->compiler = new TreeCompiler(); + $dir = $dir ?: sys_get_temp_dir(); + + if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + throw new \RuntimeException("Unable to create cache directory: $dir"); + } + + $this->cacheDir = realpath($dir); + $this->interpreter = new TreeInterpreter(); + } + + /** + * Returns data from the provided input that matches a given JMESPath + * expression. + * + * @param string $expression JMESPath expression to evaluate + * @param mixed $data Data to search. This data should be data that + * is similar to data returned from json_decode + * using associative arrays rather than objects. + * + * @return mixed Returns the matching data or null + * @throws \RuntimeException + */ + public function __invoke($expression, $data) + { + $functionName = 'jmespath_' . md5($expression); + + if (!function_exists($functionName)) { + $filename = "{$this->cacheDir}/{$functionName}.php"; + if (!file_exists($filename)) { + $this->compile($filename, $expression, $functionName); + } + require $filename; + } + + return $functionName($this->interpreter, $data); + } + + private function compile($filename, $expression, $functionName) + { + $code = $this->compiler->visit( + $this->parser->parse($expression), + $functionName, + $expression + ); + + if (!file_put_contents($filename, $code)) { + throw new \RuntimeException(sprintf( + 'Unable to write the compiled PHP code to: %s (%s)', + $filename, + var_export(error_get_last(), true) + )); + } + } +} diff --git a/vendor/mtdowling/jmespath.php/src/DebugRuntime.php b/vendor/mtdowling/jmespath.php/src/DebugRuntime.php new file mode 100644 index 0000000..4052561 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/DebugRuntime.php @@ -0,0 +1,109 @@ +<?php +namespace JmesPath; + +/** + * Provides CLI debugging information for the AST and Compiler runtimes. + */ +class DebugRuntime +{ + private $runtime; + private $out; + private $lexer; + private $parser; + + public function __construct(callable $runtime, $output = null) + { + $this->runtime = $runtime; + $this->out = $output ?: STDOUT; + $this->lexer = new Lexer(); + $this->parser = new Parser($this->lexer); + } + + public function __invoke($expression, $data) + { + if ($this->runtime instanceof CompilerRuntime) { + return $this->debugCompiled($expression, $data); + } + + return $this->debugInterpreted($expression, $data); + } + + private function debugInterpreted($expression, $data) + { + return $this->debugCallback( + function () use ($expression, $data) { + $runtime = $this->runtime; + return $runtime($expression, $data); + }, + $expression, + $data + ); + } + + private function debugCompiled($expression, $data) + { + $result = $this->debugCallback( + function () use ($expression, $data) { + $runtime = $this->runtime; + return $runtime($expression, $data); + }, + $expression, + $data + ); + $this->dumpCompiledCode($expression); + + return $result; + } + + private function dumpTokens($expression) + { + $lexer = new Lexer(); + fwrite($this->out, "Tokens\n======\n\n"); + $tokens = $lexer->tokenize($expression); + + foreach ($tokens as $t) { + fprintf( + $this->out, + "%3d %-13s %s\n", $t['pos'], $t['type'], + json_encode($t['value']) + ); + } + + fwrite($this->out, "\n"); + } + + private function dumpAst($expression) + { + $parser = new Parser(); + $ast = $parser->parse($expression); + fwrite($this->out, "AST\n========\n\n"); + fwrite($this->out, json_encode($ast, JSON_PRETTY_PRINT) . "\n"); + } + + private function dumpCompiledCode($expression) + { + fwrite($this->out, "Code\n========\n\n"); + $dir = sys_get_temp_dir(); + $hash = md5($expression); + $functionName = "jmespath_{$hash}"; + $filename = "{$dir}/{$functionName}.php"; + fwrite($this->out, "File: {$filename}\n\n"); + fprintf($this->out, file_get_contents($filename)); + } + + private function debugCallback(callable $debugFn, $expression, $data) + { + fprintf($this->out, "Expression\n==========\n\n%s\n\n", $expression); + $this->dumpTokens($expression); + $this->dumpAst($expression); + fprintf($this->out, "\nData\n====\n\n%s\n\n", json_encode($data, JSON_PRETTY_PRINT)); + $startTime = microtime(true); + $result = $debugFn(); + $total = microtime(true) - $startTime; + fprintf($this->out, "\nResult\n======\n\n%s\n\n", json_encode($result, JSON_PRETTY_PRINT)); + fwrite($this->out, "Time\n====\n\n"); + fprintf($this->out, "Total time: %f ms\n\n", $total); + + return $result; + } +} diff --git a/vendor/mtdowling/jmespath.php/src/Env.php b/vendor/mtdowling/jmespath.php/src/Env.php new file mode 100644 index 0000000..b22cf25 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/Env.php @@ -0,0 +1,91 @@ +<?php +namespace JmesPath; + +/** + * Provides a simple environment based search. + * + * The runtime utilized by the Env class can be customized via environment + * variables. If the JP_PHP_COMPILE environment variable is specified, then the + * CompilerRuntime will be utilized. If set to "on", JMESPath expressions will + * be cached to the system's temp directory. Set the environment variable to + * a string to cache expressions to a specific directory. + */ +final class Env +{ + const COMPILE_DIR = 'JP_PHP_COMPILE'; + + /** + * Returns data from the input array that matches a JMESPath expression. + * + * @param string $expression JMESPath expression to evaluate + * @param mixed $data JSON-like data to search + * + * @return mixed Returns the matching data or null + */ + public static function search($expression, $data) + { + static $runtime; + + if (!$runtime) { + $runtime = Env::createRuntime(); + } + + return $runtime($expression, $data); + } + + /** + * Creates a JMESPath runtime based on environment variables and extensions + * available on a system. + * + * @return callable + */ + public static function createRuntime() + { + switch ($compileDir = self::getEnvVariable(self::COMPILE_DIR)) { + case false: return new AstRuntime(); + case 'on': return new CompilerRuntime(); + default: return new CompilerRuntime($compileDir); + } + } + + /** + * Delete all previously compiled JMESPath files from the JP_COMPILE_DIR + * directory or sys_get_temp_dir(). + * + * @return int Returns the number of deleted files. + */ + public static function cleanCompileDir() + { + $total = 0; + $compileDir = self::getEnvVariable(self::COMPILE_DIR) ?: sys_get_temp_dir(); + + foreach (glob("{$compileDir}/jmespath_*.php") as $file) { + $total++; + unlink($file); + } + + return $total; + } + + /** + * Reads an environment variable from $_SERVER, $_ENV or via getenv(). + * + * @param string $name + * + * @return string|null + */ + private static function getEnvVariable($name) + { + if (array_key_exists($name, $_SERVER)) { + return $_SERVER[$name]; + } + + if (array_key_exists($name, $_ENV)) { + return $_ENV[$name]; + } + + $value = getenv($name); + + return $value === false ? null : $value; + } +} diff --git a/vendor/mtdowling/jmespath.php/src/FnDispatcher.php b/vendor/mtdowling/jmespath.php/src/FnDispatcher.php new file mode 100644 index 0000000..0af3ca7 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/FnDispatcher.php @@ -0,0 +1,407 @@ +<?php +namespace JmesPath; + +/** + * Dispatches to named JMESPath functions using a single function that has the + * following signature: + * + * mixed $result = fn(string $function_name, array $args) + */ +class FnDispatcher +{ + /** + * Gets a cached instance of the default function implementations. + * + * @return FnDispatcher + */ + public static function getInstance() + { + static $instance = null; + if (!$instance) { + $instance = new self(); + } + + return $instance; + } + + /** + * @param string $fn Function name. + * @param array $args Function arguments. + * + * @return mixed + */ + public function __invoke($fn, array $args) + { + return $this->{'fn_' . $fn}($args); + } + + private function fn_abs(array $args) + { + $this->validate('abs', $args, [['number']]); + return abs($args[0]); + } + + private function fn_avg(array $args) + { + $this->validate('avg', $args, [['array']]); + $sum = $this->reduce('avg:0', $args[0], ['number'], function ($a, $b) { + return Utils::add($a, $b); + }); + return $args[0] ? ($sum / count($args[0])) : null; + } + + private function fn_ceil(array $args) + { + $this->validate('ceil', $args, [['number']]); + return ceil($args[0]); + } + + private function fn_contains(array $args) + { + $this->validate('contains', $args, [['string', 'array'], ['any']]); + if (is_array($args[0])) { + return in_array($args[1], $args[0]); + } elseif (is_string($args[1])) { + return mb_strpos($args[0], $args[1], 0, 'UTF-8') !== false; + } else { + return null; + } + } + + private function fn_ends_with(array $args) + { + $this->validate('ends_with', $args, [['string'], ['string']]); + list($search, $suffix) = $args; + return $suffix === '' || mb_substr($search, -mb_strlen($suffix, 'UTF-8'), null, 'UTF-8') === $suffix; + } + + private function fn_floor(array $args) + { + $this->validate('floor', $args, [['number']]); + return floor($args[0]); + } + + private function fn_not_null(array $args) + { + if (!$args) { + throw new \RuntimeException( + "not_null() expects 1 or more arguments, 0 were provided" + ); + } + + return array_reduce($args, function ($carry, $item) { + return $carry !== null ? $carry : $item; + }); + } + + private function fn_join(array $args) + { + $this->validate('join', $args, [['string'], ['array']]); + $fn = function ($a, $b, $i) use ($args) { + return $i ? ($a . $args[0] . $b) : $b; + }; + return $this->reduce('join:0', $args[1], ['string'], $fn); + } + + private function fn_keys(array $args) + { + $this->validate('keys', $args, [['object']]); + return array_keys((array) $args[0]); + } + + private function fn_length(array $args) + { + $this->validate('length', $args, [['string', 'array', 'object']]); + return is_string($args[0]) ? mb_strlen($args[0], 'UTF-8') : count((array) $args[0]); + } + + private function fn_max(array $args) + { + $this->validate('max', $args, [['array']]); + $fn = function ($a, $b) { + return $a >= $b ? $a : $b; + }; + return $this->reduce('max:0', $args[0], ['number', 'string'], $fn); + } + + private function fn_max_by(array $args) + { + $this->validate('max_by', $args, [['array'], ['expression']]); + $expr = $this->wrapExpression('max_by:1', $args[1], ['number', 'string']); + $fn = function ($carry, $item, $index) use ($expr) { + return $index + ? ($expr($carry) >= $expr($item) ? $carry : $item) + : $item; + }; + return $this->reduce('max_by:1', $args[0], ['any'], $fn); + } + + private function fn_min(array $args) + { + $this->validate('min', $args, [['array']]); + $fn = function ($a, $b, $i) { + return $i && $a <= $b ? $a : $b; + }; + return $this->reduce('min:0', $args[0], ['number', 'string'], $fn); + } + + private function fn_min_by(array $args) + { + $this->validate('min_by', $args, [['array'], ['expression']]); + $expr = $this->wrapExpression('min_by:1', $args[1], ['number', 'string']); + $i = -1; + $fn = function ($a, $b) use ($expr, &$i) { + return ++$i ? ($expr($a) <= $expr($b) ? $a : $b) : $b; + }; + return $this->reduce('min_by:1', $args[0], ['any'], $fn); + } + + private function fn_reverse(array $args) + { + $this->validate('reverse', $args, [['array', 'string']]); + if (is_array($args[0])) { + return array_reverse($args[0]); + } elseif (is_string($args[0])) { + return strrev($args[0]); + } else { + throw new \RuntimeException('Cannot reverse provided argument'); + } + } + + private function fn_sum(array $args) + { + $this->validate('sum', $args, [['array']]); + $fn = function ($a, $b) { + return Utils::add($a, $b); + }; + return $this->reduce('sum:0', $args[0], ['number'], $fn); + } + + private function fn_sort(array $args) + { + $this->validate('sort', $args, [['array']]); + $valid = ['string', 'number']; + return Utils::stableSort($args[0], function ($a, $b) use ($valid) { + $this->validateSeq('sort:0', $valid, $a, $b); + return strnatcmp($a, $b); + }); + } + + private function fn_sort_by(array $args) + { + $this->validate('sort_by', $args, [['array'], ['expression']]); + $expr = $args[1]; + $valid = ['string', 'number']; + return Utils::stableSort( + $args[0], + function ($a, $b) use ($expr, $valid) { + $va = $expr($a); + $vb = $expr($b); + $this->validateSeq('sort_by:0', $valid, $va, $vb); + return strnatcmp($va, $vb); + } + ); + } + + private function fn_starts_with(array $args) + { + $this->validate('starts_with', $args, [['string'], ['string']]); + list($search, $prefix) = $args; + return $prefix === '' || mb_strpos($search, $prefix, 0, 'UTF-8') === 0; + } + + private function fn_type(array $args) + { + $this->validateArity('type', count($args), 1); + return Utils::type($args[0]); + } + + private function fn_to_string(array $args) + { + $this->validateArity('to_string', count($args), 1); + $v = $args[0]; + if (is_string($v)) { + return $v; + } elseif (is_object($v) + && !($v instanceof \JsonSerializable) + && method_exists($v, '__toString') + ) { + return (string) $v; + } + + return json_encode($v); + } + + private function fn_to_number(array $args) + { + $this->validateArity('to_number', count($args), 1); + $value = $args[0]; + $type = Utils::type($value); + if ($type == 'number') { + return $value; + } elseif ($type == 'string' && is_numeric($value)) { + return mb_strpos($value, '.', 0, 'UTF-8') ? (float) $value : (int) $value; + } else { + return null; + } + } + + private function fn_values(array $args) + { + $this->validate('values', $args, [['array', 'object']]); + return array_values((array) $args[0]); + } + + private function fn_merge(array $args) + { + if (!$args) { + throw new \RuntimeException( + "merge() expects 1 or more arguments, 0 were provided" + ); + } + + return call_user_func_array('array_replace', $args); + } + + private function fn_to_array(array $args) + { + $this->validate('to_array', $args, [['any']]); + + return Utils::isArray($args[0]) ? $args[0] : [$args[0]]; + } + + private function fn_map(array $args) + { + $this->validate('map', $args, [['expression'], ['any']]); + $result = []; + foreach ($args[1] as $a) { + $result[] = $args[0]($a); + } + return $result; + } + + private function typeError($from, $msg) + { + if (mb_strpos($from, ':', 0, 'UTF-8')) { + list($fn, $pos) = explode(':', $from); + throw new \RuntimeException( + sprintf('Argument %d of %s %s', $pos, $fn, $msg) + ); + } else { + throw new \RuntimeException( + sprintf('Type error: %s %s', $from, $msg) + ); + } + } + + private function validateArity($from, $given, $expected) + { + if ($given != $expected) { + $err = "%s() expects {$expected} arguments, {$given} were provided"; + throw new \RuntimeException(sprintf($err, $from)); + } + } + + private function validate($from, $args, $types = []) + { + $this->validateArity($from, count($args), count($types)); + foreach ($args as $index => $value) { + if (!isset($types[$index]) || !$types[$index]) { + continue; + } + $this->validateType("{$from}:{$index}", $value, $types[$index]); + } + } + + private function validateType($from, $value, array $types) + { + if ($types[0] == 'any' + || in_array(Utils::type($value), $types) + || ($value === [] && in_array('object', $types)) + ) { + return; + } + $msg = 'must be one of the following types: ' . implode(', ', $types) + . '. ' . Utils::type($value) . ' found'; + $this->typeError($from, $msg); + } + + /** + * Validates value A and B, ensures they both are correctly typed, and of + * the same type. + * + * @param string $from String of function:argument_position + * @param array $types Array of valid value types. + * @param mixed $a Value A + * @param mixed $b Value B + */ + private function validateSeq($from, array $types, $a, $b) + { + $ta = Utils::type($a); + $tb = Utils::type($b); + + if ($ta !== $tb) { + $msg = "encountered a type mismatch in sequence: {$ta}, {$tb}"; + $this->typeError($from, $msg); + } + + $typeMatch = ($types && $types[0] == 'any') || in_array($ta, $types); + if (!$typeMatch) { + $msg = 'encountered a type error in sequence. The argument must be ' + . 'an array of ' . implode('|', $types) . ' types. ' + . "Found {$ta}, {$tb}."; + $this->typeError($from, $msg); + } + } + + /** + * Reduces and validates an array of values to a single value using a fn. + * + * @param string $from String of function:argument_position + * @param array $values Values to reduce. + * @param array $types Array of valid value types. + * @param callable $reduce Reduce function that accepts ($carry, $item). + * + * @return mixed + */ + private function reduce($from, array $values, array $types, callable $reduce) + { + $i = -1; + return array_reduce( + $values, + function ($carry, $item) use ($from, $types, $reduce, &$i) { + if (++$i > 0) { + $this->validateSeq($from, $types, $carry, $item); + } + return $reduce($carry, $item, $i); + } + ); + } + + /** + * Validates the return values of expressions as they are applied. + * + * @param string $from Function name : position + * @param callable $expr Expression function to validate. + * @param array $types Array of acceptable return type values. + * + * @return callable Returns a wrapped function + */ + private function wrapExpression($from, callable $expr, array $types) + { + list($fn, $pos) = explode(':', $from); + $from = "The expression return value of argument {$pos} of {$fn}"; + return function ($value) use ($from, $expr, $types) { + $value = $expr($value); + $this->validateType($from, $value, $types); + return $value; + }; + } + + /** @internal Pass function name validation off to runtime */ + public function __call($name, $args) + { + $name = str_replace('fn_', '', $name); + throw new \RuntimeException("Call to undefined function {$name}"); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/JmesPath.php b/vendor/mtdowling/jmespath.php/src/JmesPath.php new file mode 100644 index 0000000..d24e516 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/JmesPath.php @@ -0,0 +1,17 @@ +<?php +namespace JmesPath; + +/** + * Returns data from the input array that matches a JMESPath expression. + * + * @param string $expression Expression to search. + * @param mixed $data Data to search. + * + * @return mixed + */ +if (!function_exists(__NAMESPACE__ . '\search')) { + function search($expression, $data) + { + return Env::search($expression, $data); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/Lexer.php b/vendor/mtdowling/jmespath.php/src/Lexer.php new file mode 100644 index 0000000..c98ffb6 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/Lexer.php @@ -0,0 +1,444 @@ +<?php +namespace JmesPath; + +/** + * Tokenizes JMESPath expressions + */ +class Lexer +{ + const T_DOT = 'dot'; + const T_STAR = 'star'; + const T_COMMA = 'comma'; + const T_COLON = 'colon'; + const T_CURRENT = 'current'; + const T_EXPREF = 'expref'; + const T_LPAREN = 'lparen'; + const T_RPAREN = 'rparen'; + const T_LBRACE = 'lbrace'; + const T_RBRACE = 'rbrace'; + const T_LBRACKET = 'lbracket'; + const T_RBRACKET = 'rbracket'; + const T_FLATTEN = 'flatten'; + const T_IDENTIFIER = 'identifier'; + const T_NUMBER = 'number'; + const T_QUOTED_IDENTIFIER = 'quoted_identifier'; + const T_UNKNOWN = 'unknown'; + const T_PIPE = 'pipe'; + const T_OR = 'or'; + const T_AND = 'and'; + const T_NOT = 'not'; + const T_FILTER = 'filter'; + const T_LITERAL = 'literal'; + const T_EOF = 'eof'; + const T_COMPARATOR = 'comparator'; + + const STATE_IDENTIFIER = 0; + const STATE_NUMBER = 1; + const STATE_SINGLE_CHAR = 2; + const STATE_WHITESPACE = 3; + const STATE_STRING_LITERAL = 4; + const STATE_QUOTED_STRING = 5; + const STATE_JSON_LITERAL = 6; + const STATE_LBRACKET = 7; + const STATE_PIPE = 8; + const STATE_LT = 9; + const STATE_GT = 10; + const STATE_EQ = 11; + const STATE_NOT = 12; + const STATE_AND = 13; + + /** @var array We know what token we are consuming based on each char */ + private static $transitionTable = [ + '<' => self::STATE_LT, + '>' => self::STATE_GT, + '=' => self::STATE_EQ, + '!' => self::STATE_NOT, + '[' => self::STATE_LBRACKET, + '|' => self::STATE_PIPE, + '&' => self::STATE_AND, + '`' => self::STATE_JSON_LITERAL, + '"' => self::STATE_QUOTED_STRING, + "'" => self::STATE_STRING_LITERAL, + '-' => self::STATE_NUMBER, + '0' => self::STATE_NUMBER, + '1' => self::STATE_NUMBER, + '2' => self::STATE_NUMBER, + '3' => self::STATE_NUMBER, + '4' => self::STATE_NUMBER, + '5' => self::STATE_NUMBER, + '6' => self::STATE_NUMBER, + '7' => self::STATE_NUMBER, + '8' => self::STATE_NUMBER, + '9' => self::STATE_NUMBER, + ' ' => self::STATE_WHITESPACE, + "\t" => self::STATE_WHITESPACE, + "\n" => self::STATE_WHITESPACE, + "\r" => self::STATE_WHITESPACE, + '.' => self::STATE_SINGLE_CHAR, + '*' => self::STATE_SINGLE_CHAR, + ']' => self::STATE_SINGLE_CHAR, + ',' => self::STATE_SINGLE_CHAR, + ':' => self::STATE_SINGLE_CHAR, + '@' => self::STATE_SINGLE_CHAR, + '(' => self::STATE_SINGLE_CHAR, + ')' => self::STATE_SINGLE_CHAR, + '{' => self::STATE_SINGLE_CHAR, + '}' => self::STATE_SINGLE_CHAR, + '_' => self::STATE_IDENTIFIER, + 'A' => self::STATE_IDENTIFIER, + 'B' => self::STATE_IDENTIFIER, + 'C' => self::STATE_IDENTIFIER, + 'D' => self::STATE_IDENTIFIER, + 'E' => self::STATE_IDENTIFIER, + 'F' => self::STATE_IDENTIFIER, + 'G' => self::STATE_IDENTIFIER, + 'H' => self::STATE_IDENTIFIER, + 'I' => self::STATE_IDENTIFIER, + 'J' => self::STATE_IDENTIFIER, + 'K' => self::STATE_IDENTIFIER, + 'L' => self::STATE_IDENTIFIER, + 'M' => self::STATE_IDENTIFIER, + 'N' => self::STATE_IDENTIFIER, + 'O' => self::STATE_IDENTIFIER, + 'P' => self::STATE_IDENTIFIER, + 'Q' => self::STATE_IDENTIFIER, + 'R' => self::STATE_IDENTIFIER, + 'S' => self::STATE_IDENTIFIER, + 'T' => self::STATE_IDENTIFIER, + 'U' => self::STATE_IDENTIFIER, + 'V' => self::STATE_IDENTIFIER, + 'W' => self::STATE_IDENTIFIER, + 'X' => self::STATE_IDENTIFIER, + 'Y' => self::STATE_IDENTIFIER, + 'Z' => self::STATE_IDENTIFIER, + 'a' => self::STATE_IDENTIFIER, + 'b' => self::STATE_IDENTIFIER, + 'c' => self::STATE_IDENTIFIER, + 'd' => self::STATE_IDENTIFIER, + 'e' => self::STATE_IDENTIFIER, + 'f' => self::STATE_IDENTIFIER, + 'g' => self::STATE_IDENTIFIER, + 'h' => self::STATE_IDENTIFIER, + 'i' => self::STATE_IDENTIFIER, + 'j' => self::STATE_IDENTIFIER, + 'k' => self::STATE_IDENTIFIER, + 'l' => self::STATE_IDENTIFIER, + 'm' => self::STATE_IDENTIFIER, + 'n' => self::STATE_IDENTIFIER, + 'o' => self::STATE_IDENTIFIER, + 'p' => self::STATE_IDENTIFIER, + 'q' => self::STATE_IDENTIFIER, + 'r' => self::STATE_IDENTIFIER, + 's' => self::STATE_IDENTIFIER, + 't' => self::STATE_IDENTIFIER, + 'u' => self::STATE_IDENTIFIER, + 'v' => self::STATE_IDENTIFIER, + 'w' => self::STATE_IDENTIFIER, + 'x' => self::STATE_IDENTIFIER, + 'y' => self::STATE_IDENTIFIER, + 'z' => self::STATE_IDENTIFIER, + ]; + + /** @var array Valid identifier characters after first character */ + private $validIdentifier = [ + 'A' => true, 'B' => true, 'C' => true, 'D' => true, 'E' => true, + 'F' => true, 'G' => true, 'H' => true, 'I' => true, 'J' => true, + 'K' => true, 'L' => true, 'M' => true, 'N' => true, 'O' => true, + 'P' => true, 'Q' => true, 'R' => true, 'S' => true, 'T' => true, + 'U' => true, 'V' => true, 'W' => true, 'X' => true, 'Y' => true, + 'Z' => true, 'a' => true, 'b' => true, 'c' => true, 'd' => true, + 'e' => true, 'f' => true, 'g' => true, 'h' => true, 'i' => true, + 'j' => true, 'k' => true, 'l' => true, 'm' => true, 'n' => true, + 'o' => true, 'p' => true, 'q' => true, 'r' => true, 's' => true, + 't' => true, 'u' => true, 'v' => true, 'w' => true, 'x' => true, + 'y' => true, 'z' => true, '_' => true, '0' => true, '1' => true, + '2' => true, '3' => true, '4' => true, '5' => true, '6' => true, + '7' => true, '8' => true, '9' => true, + ]; + + /** @var array Valid number characters after the first character */ + private $numbers = [ + '0' => true, '1' => true, '2' => true, '3' => true, '4' => true, + '5' => true, '6' => true, '7' => true, '8' => true, '9' => true + ]; + + /** @var array Map of simple single character tokens */ + private $simpleTokens = [ + '.' => self::T_DOT, + '*' => self::T_STAR, + ']' => self::T_RBRACKET, + ',' => self::T_COMMA, + ':' => self::T_COLON, + '@' => self::T_CURRENT, + '(' => self::T_LPAREN, + ')' => self::T_RPAREN, + '{' => self::T_LBRACE, + '}' => self::T_RBRACE, + ]; + + /** + * Tokenize the JMESPath expression into an array of tokens hashes that + * contain a 'type', 'value', and 'key'. + * + * @param string $input JMESPath input + * + * @return array + * @throws SyntaxErrorException + */ + public function tokenize($input) + { + $tokens = []; + + if ($input === '') { + goto eof; + } + + $chars = str_split($input); + + while (false !== ($current = current($chars))) { + + // Every character must be in the transition character table. + if (!isset(self::$transitionTable[$current])) { + $tokens[] = [ + 'type' => self::T_UNKNOWN, + 'pos' => key($chars), + 'value' => $current + ]; + next($chars); + continue; + } + + $state = self::$transitionTable[$current]; + + if ($state === self::STATE_SINGLE_CHAR) { + + // Consume simple tokens like ".", ",", "@", etc. + $tokens[] = [ + 'type' => $this->simpleTokens[$current], + 'pos' => key($chars), + 'value' => $current + ]; + next($chars); + + } elseif ($state === self::STATE_IDENTIFIER) { + + // Consume identifiers + $start = key($chars); + $buffer = ''; + do { + $buffer .= $current; + $current = next($chars); + } while ($current !== false && isset($this->validIdentifier[$current])); + $tokens[] = [ + 'type' => self::T_IDENTIFIER, + 'value' => $buffer, + 'pos' => $start + ]; + + } elseif ($state === self::STATE_WHITESPACE) { + + // Skip whitespace + next($chars); + + } elseif ($state === self::STATE_LBRACKET) { + + // Consume "[", "[?", and "[]" + $position = key($chars); + $actual = next($chars); + if ($actual === ']') { + next($chars); + $tokens[] = [ + 'type' => self::T_FLATTEN, + 'pos' => $position, + 'value' => '[]' + ]; + } elseif ($actual === '?') { + next($chars); + $tokens[] = [ + 'type' => self::T_FILTER, + 'pos' => $position, + 'value' => '[?' + ]; + } else { + $tokens[] = [ + 'type' => self::T_LBRACKET, + 'pos' => $position, + 'value' => '[' + ]; + } + + } elseif ($state === self::STATE_STRING_LITERAL) { + + // Consume raw string literals + $t = $this->inside($chars, "'", self::T_LITERAL); + $t['value'] = str_replace("\\'", "'", $t['value']); + $tokens[] = $t; + + } elseif ($state === self::STATE_PIPE) { + + // Consume pipe and OR + $tokens[] = $this->matchOr($chars, '|', '|', self::T_OR, self::T_PIPE); + + } elseif ($state == self::STATE_JSON_LITERAL) { + + // Consume JSON literals + $token = $this->inside($chars, '`', self::T_LITERAL); + if ($token['type'] === self::T_LITERAL) { + $token['value'] = str_replace('\\`', '`', $token['value']); + $token = $this->parseJson($token); + } + $tokens[] = $token; + + } elseif ($state == self::STATE_NUMBER) { + + // Consume numbers + $start = key($chars); + $buffer = ''; + do { + $buffer .= $current; + $current = next($chars); + } while ($current !== false && isset($this->numbers[$current])); + $tokens[] = [ + 'type' => self::T_NUMBER, + 'value' => (int)$buffer, + 'pos' => $start + ]; + + } elseif ($state === self::STATE_QUOTED_STRING) { + + // Consume quoted identifiers + $token = $this->inside($chars, '"', self::T_QUOTED_IDENTIFIER); + if ($token['type'] === self::T_QUOTED_IDENTIFIER) { + $token['value'] = '"' . $token['value'] . '"'; + $token = $this->parseJson($token); + } + $tokens[] = $token; + + } elseif ($state === self::STATE_EQ) { + + // Consume equals + $tokens[] = $this->matchOr($chars, '=', '=', self::T_COMPARATOR, self::T_UNKNOWN); + + } elseif ($state == self::STATE_AND) { + + $tokens[] = $this->matchOr($chars, '&', '&', self::T_AND, self::T_EXPREF); + + } elseif ($state === self::STATE_NOT) { + + // Consume not equal + $tokens[] = $this->matchOr($chars, '!', '=', self::T_COMPARATOR, self::T_NOT); + + } else { + + // either '<' or '>' + // Consume less than and greater than + $tokens[] = $this->matchOr($chars, $current, '=', self::T_COMPARATOR, self::T_COMPARATOR); + + } + } + + eof: + $tokens[] = [ + 'type' => self::T_EOF, + 'pos' => mb_strlen($input, 'UTF-8'), + 'value' => null + ]; + + return $tokens; + } + + /** + * Returns a token based on whether or not the next token matches the + * expected value. If it does, a token of "$type" is returned. Otherwise, + * a token of "$orElse" type is returned. + * + * @param array $chars Array of characters by reference. + * @param string $current The current character. + * @param string $expected Expected character. + * @param string $type Expected result type. + * @param string $orElse Otherwise return a token of this type. + * + * @return array Returns a conditional token. + */ + private function matchOr(array &$chars, $current, $expected, $type, $orElse) + { + if (next($chars) === $expected) { + next($chars); + return [ + 'type' => $type, + 'pos' => key($chars) - 1, + 'value' => $current . $expected + ]; + } + + return [ + 'type' => $orElse, + 'pos' => key($chars) - 1, + 'value' => $current + ]; + } + + /** + * Returns a token the is the result of consuming inside of delimiter + * characters. Escaped delimiters will be adjusted before returning a + * value. If the token is not closed, "unknown" is returned. + * + * @param array $chars Array of characters by reference. + * @param string $delim The delimiter character. + * @param string $type Token type. + * + * @return array Returns the consumed token. + */ + private function inside(array &$chars, $delim, $type) + { + $position = key($chars); + $current = next($chars); + $buffer = ''; + + while ($current !== $delim) { + if ($current === '\\') { + $buffer .= '\\'; + $current = next($chars); + } + if ($current === false) { + // Unclosed delimiter + return [ + 'type' => self::T_UNKNOWN, + 'value' => $buffer, + 'pos' => $position + ]; + } + $buffer .= $current; + $current = next($chars); + } + + next($chars); + + return ['type' => $type, 'value' => $buffer, 'pos' => $position]; + } + + /** + * Parses a JSON token or sets the token type to "unknown" on error. + * + * @param array $token Token that needs parsing. + * + * @return array Returns a token with a parsed value. + */ + private function parseJson(array $token) + { + $value = json_decode($token['value'], true); + + if ($error = json_last_error()) { + // Legacy support for elided quotes. Try to parse again by adding + // quotes around the bad input value. + $value = json_decode('"' . $token['value'] . '"', true); + if ($error = json_last_error()) { + $token['type'] = self::T_UNKNOWN; + return $token; + } + } + + $token['value'] = $value; + return $token; + } +} diff --git a/vendor/mtdowling/jmespath.php/src/Parser.php b/vendor/mtdowling/jmespath.php/src/Parser.php new file mode 100644 index 0000000..0733f20 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/Parser.php @@ -0,0 +1,519 @@ +<?php +namespace JmesPath; + +use JmesPath\Lexer as T; + +/** + * JMESPath Pratt parser + * @link http://hall.org.ua/halls/wizzard/pdf/Vaughan.Pratt.TDOP.pdf + */ +class Parser +{ + /** @var Lexer */ + private $lexer; + private $tokens; + private $token; + private $tpos; + private $expression; + private static $nullToken = ['type' => T::T_EOF]; + private static $currentNode = ['type' => T::T_CURRENT]; + + private static $bp = [ + T::T_EOF => 0, + T::T_QUOTED_IDENTIFIER => 0, + T::T_IDENTIFIER => 0, + T::T_RBRACKET => 0, + T::T_RPAREN => 0, + T::T_COMMA => 0, + T::T_RBRACE => 0, + T::T_NUMBER => 0, + T::T_CURRENT => 0, + T::T_EXPREF => 0, + T::T_COLON => 0, + T::T_PIPE => 1, + T::T_OR => 2, + T::T_AND => 3, + T::T_COMPARATOR => 5, + T::T_FLATTEN => 9, + T::T_STAR => 20, + T::T_FILTER => 21, + T::T_DOT => 40, + T::T_NOT => 45, + T::T_LBRACE => 50, + T::T_LBRACKET => 55, + T::T_LPAREN => 60, + ]; + + /** @var array Acceptable tokens after a dot token */ + private static $afterDot = [ + T::T_IDENTIFIER => true, // foo.bar + T::T_QUOTED_IDENTIFIER => true, // foo."bar" + T::T_STAR => true, // foo.* + T::T_LBRACE => true, // foo[1] + T::T_LBRACKET => true, // foo{a: 0} + T::T_FILTER => true, // foo.[?bar==10] + ]; + + /** + * @param Lexer|null $lexer Lexer used to tokenize expressions + */ + public function __construct(Lexer $lexer = null) + { + $this->lexer = $lexer ?: new Lexer(); + } + + /** + * Parses a JMESPath expression into an AST + * + * @param string $expression JMESPath expression to compile + * + * @return array Returns an array based AST + * @throws SyntaxErrorException + */ + public function parse($expression) + { + $this->expression = $expression; + $this->tokens = $this->lexer->tokenize($expression); + $this->tpos = -1; + $this->next(); + $result = $this->expr(); + + if ($this->token['type'] === T::T_EOF) { + return $result; + } + + throw $this->syntax('Did not reach the end of the token stream'); + } + + /** + * Parses an expression while rbp < lbp. + * + * @param int $rbp Right bound precedence + * + * @return array + */ + private function expr($rbp = 0) + { + $left = $this->{"nud_{$this->token['type']}"}(); + while ($rbp < self::$bp[$this->token['type']]) { + $left = $this->{"led_{$this->token['type']}"}($left); + } + + return $left; + } + + private function nud_identifier() + { + $token = $this->token; + $this->next(); + return ['type' => 'field', 'value' => $token['value']]; + } + + private function nud_quoted_identifier() + { + $token = $this->token; + $this->next(); + $this->assertNotToken(T::T_LPAREN); + return ['type' => 'field', 'value' => $token['value']]; + } + + private function nud_current() + { + $this->next(); + return self::$currentNode; + } + + private function nud_literal() + { + $token = $this->token; + $this->next(); + return ['type' => 'literal', 'value' => $token['value']]; + } + + private function nud_expref() + { + $this->next(); + return ['type' => T::T_EXPREF, 'children' => [$this->expr(self::$bp[T::T_EXPREF])]]; + } + + private function nud_not() + { + $this->next(); + return ['type' => T::T_NOT, 'children' => [$this->expr(self::$bp[T::T_NOT])]]; + } + + private function nud_lparen() + { + $this->next(); + $result = $this->expr(0); + if ($this->token['type'] !== T::T_RPAREN) { + throw $this->syntax('Unclosed `(`'); + } + $this->next(); + return $result; + } + + private function nud_lbrace() + { + static $validKeys = [T::T_QUOTED_IDENTIFIER => true, T::T_IDENTIFIER => true]; + $this->next($validKeys); + $pairs = []; + + do { + $pairs[] = $this->parseKeyValuePair(); + if ($this->token['type'] == T::T_COMMA) { + $this->next($validKeys); + } + } while ($this->token['type'] !== T::T_RBRACE); + + $this->next(); + + return['type' => 'multi_select_hash', 'children' => $pairs]; + } + + private function nud_flatten() + { + return $this->led_flatten(self::$currentNode); + } + + private function nud_filter() + { + return $this->led_filter(self::$currentNode); + } + + private function nud_star() + { + return $this->parseWildcardObject(self::$currentNode); + } + + private function nud_lbracket() + { + $this->next(); + $type = $this->token['type']; + if ($type == T::T_NUMBER || $type == T::T_COLON) { + return $this->parseArrayIndexExpression(); + } elseif ($type == T::T_STAR && $this->lookahead() == T::T_RBRACKET) { + return $this->parseWildcardArray(); + } else { + return $this->parseMultiSelectList(); + } + } + + private function led_lbracket(array $left) + { + static $nextTypes = [T::T_NUMBER => true, T::T_COLON => true, T::T_STAR => true]; + $this->next($nextTypes); + switch ($this->token['type']) { + case T::T_NUMBER: + case T::T_COLON: + return [ + 'type' => 'subexpression', + 'children' => [$left, $this->parseArrayIndexExpression()] + ]; + default: + return $this->parseWildcardArray($left); + } + } + + private function led_flatten(array $left) + { + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + ['type' => T::T_FLATTEN, 'children' => [$left]], + $this->parseProjection(self::$bp[T::T_FLATTEN]) + ] + ]; + } + + private function led_dot(array $left) + { + $this->next(self::$afterDot); + + if ($this->token['type'] == T::T_STAR) { + return $this->parseWildcardObject($left); + } + + return [ + 'type' => 'subexpression', + 'children' => [$left, $this->parseDot(self::$bp[T::T_DOT])] + ]; + } + + private function led_or(array $left) + { + $this->next(); + return [ + 'type' => T::T_OR, + 'children' => [$left, $this->expr(self::$bp[T::T_OR])] + ]; + } + + private function led_and(array $left) + { + $this->next(); + return [ + 'type' => T::T_AND, + 'children' => [$left, $this->expr(self::$bp[T::T_AND])] + ]; + } + + private function led_pipe(array $left) + { + $this->next(); + return [ + 'type' => T::T_PIPE, + 'children' => [$left, $this->expr(self::$bp[T::T_PIPE])] + ]; + } + + private function led_lparen(array $left) + { + $args = []; + $this->next(); + + while ($this->token['type'] != T::T_RPAREN) { + $args[] = $this->expr(0); + if ($this->token['type'] == T::T_COMMA) { + $this->next(); + } + } + + $this->next(); + + return [ + 'type' => 'function', + 'value' => $left['value'], + 'children' => $args + ]; + } + + private function led_filter(array $left) + { + $this->next(); + $expression = $this->expr(); + if ($this->token['type'] != T::T_RBRACKET) { + throw $this->syntax('Expected a closing rbracket for the filter'); + } + + $this->next(); + $rhs = $this->parseProjection(self::$bp[T::T_FILTER]); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + $left ?: self::$currentNode, + [ + 'type' => 'condition', + 'children' => [$expression, $rhs] + ] + ] + ]; + } + + private function led_comparator(array $left) + { + $token = $this->token; + $this->next(); + + return [ + 'type' => T::T_COMPARATOR, + 'value' => $token['value'], + 'children' => [$left, $this->expr(self::$bp[T::T_COMPARATOR])] + ]; + } + + private function parseProjection($bp) + { + $type = $this->token['type']; + if (self::$bp[$type] < 10) { + return self::$currentNode; + } elseif ($type == T::T_DOT) { + $this->next(self::$afterDot); + return $this->parseDot($bp); + } elseif ($type == T::T_LBRACKET || $type == T::T_FILTER) { + return $this->expr($bp); + } + + throw $this->syntax('Syntax error after projection'); + } + + private function parseDot($bp) + { + if ($this->token['type'] == T::T_LBRACKET) { + $this->next(); + return $this->parseMultiSelectList(); + } + + return $this->expr($bp); + } + + private function parseKeyValuePair() + { + static $validColon = [T::T_COLON => true]; + $key = $this->token['value']; + $this->next($validColon); + $this->next(); + + return [ + 'type' => 'key_val_pair', + 'value' => $key, + 'children' => [$this->expr()] + ]; + } + + private function parseWildcardObject(array $left = null) + { + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'object', + 'children' => [ + $left ?: self::$currentNode, + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + private function parseWildcardArray(array $left = null) + { + static $getRbracket = [T::T_RBRACKET => true]; + $this->next($getRbracket); + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + $left ?: self::$currentNode, + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + /** + * Parses an array index expression (e.g., [0], [1:2:3] + */ + private function parseArrayIndexExpression() + { + static $matchNext = [ + T::T_NUMBER => true, + T::T_COLON => true, + T::T_RBRACKET => true + ]; + + $pos = 0; + $parts = [null, null, null]; + $expected = $matchNext; + + do { + if ($this->token['type'] == T::T_COLON) { + $pos++; + $expected = $matchNext; + } elseif ($this->token['type'] == T::T_NUMBER) { + $parts[$pos] = $this->token['value']; + $expected = [T::T_COLON => true, T::T_RBRACKET => true]; + } + $this->next($expected); + } while ($this->token['type'] != T::T_RBRACKET); + + // Consume the closing bracket + $this->next(); + + if ($pos === 0) { + // No colons were found so this is a simple index extraction + return ['type' => 'index', 'value' => $parts[0]]; + } + + if ($pos > 2) { + throw $this->syntax('Invalid array slice syntax: too many colons'); + } + + // Sliced array from start (e.g., [2:]) + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + ['type' => 'slice', 'value' => $parts], + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + private function parseMultiSelectList() + { + $nodes = []; + + do { + $nodes[] = $this->expr(); + if ($this->token['type'] == T::T_COMMA) { + $this->next(); + $this->assertNotToken(T::T_RBRACKET); + } + } while ($this->token['type'] !== T::T_RBRACKET); + $this->next(); + + return ['type' => 'multi_select_list', 'children' => $nodes]; + } + + private function syntax($msg) + { + return new SyntaxErrorException($msg, $this->token, $this->expression); + } + + private function lookahead() + { + return (!isset($this->tokens[$this->tpos + 1])) + ? T::T_EOF + : $this->tokens[$this->tpos + 1]['type']; + } + + private function next(array $match = null) + { + if (!isset($this->tokens[$this->tpos + 1])) { + $this->token = self::$nullToken; + } else { + $this->token = $this->tokens[++$this->tpos]; + } + + if ($match && !isset($match[$this->token['type']])) { + throw $this->syntax($match); + } + } + + private function assertNotToken($type) + { + if ($this->token['type'] == $type) { + throw $this->syntax("Token {$this->tpos} not allowed to be $type"); + } + } + + /** + * @internal Handles undefined tokens without paying the cost of validation + */ + public function __call($method, $args) + { + $prefix = substr($method, 0, 4); + if ($prefix == 'nud_' || $prefix == 'led_') { + $token = substr($method, 4); + $message = "Unexpected \"$token\" token ($method). Expected one of" + . " the following tokens: " + . implode(', ', array_map(function ($i) { + return '"' . substr($i, 4) . '"'; + }, array_filter( + get_class_methods($this), + function ($i) use ($prefix) { + return strpos($i, $prefix) === 0; + } + ))); + throw $this->syntax($message); + } + + throw new \BadMethodCallException("Call to undefined method $method"); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/SyntaxErrorException.php b/vendor/mtdowling/jmespath.php/src/SyntaxErrorException.php new file mode 100644 index 0000000..68683d0 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/SyntaxErrorException.php @@ -0,0 +1,36 @@ +<?php +namespace JmesPath; + +/** + * Syntax errors raise this exception that gives context + */ +class SyntaxErrorException extends \InvalidArgumentException +{ + /** + * @param string $expectedTypesOrMessage Expected array of tokens or message + * @param array $token Current token + * @param string $expression Expression input + */ + public function __construct( + $expectedTypesOrMessage, + array $token, + $expression + ) { + $message = "Syntax error at character {$token['pos']}\n" + . $expression . "\n" . str_repeat(' ', max($token['pos'], 0)) . "^\n"; + $message .= !is_array($expectedTypesOrMessage) + ? $expectedTypesOrMessage + : $this->createTokenMessage($token, $expectedTypesOrMessage); + parent::__construct($message); + } + + private function createTokenMessage(array $token, array $valid) + { + return sprintf( + 'Expected one of the following: %s; found %s "%s"', + implode(', ', array_keys($valid)), + $token['type'], + $token['value'] + ); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/TreeCompiler.php b/vendor/mtdowling/jmespath.php/src/TreeCompiler.php new file mode 100644 index 0000000..fe27f41 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/TreeCompiler.php @@ -0,0 +1,419 @@ +<?php +namespace JmesPath; + +/** + * Tree visitor used to compile JMESPath expressions into native PHP code. + */ +class TreeCompiler +{ + private $indentation; + private $source; + private $vars; + + /** + * @param array $ast AST to compile. + * @param string $fnName The name of the function to generate. + * @param string $expr Expression being compiled. + * + * @return string + */ + public function visit(array $ast, $fnName, $expr) + { + $this->vars = []; + $this->source = $this->indentation = ''; + $this->write("<?php\n") + ->write('use JmesPath\\TreeInterpreter as Ti;') + ->write('use JmesPath\\FnDispatcher as Fd;') + ->write('use JmesPath\\Utils;') + ->write('') + ->write('function %s(Ti $interpreter, $value) {', $fnName) + ->indent() + ->dispatch($ast) + ->write('') + ->write('return $value;') + ->outdent() + ->write('}'); + + return $this->source; + } + + /** + * @param array $node + * @return mixed + */ + private function dispatch(array $node) + { + return $this->{"visit_{$node['type']}"}($node); + } + + /** + * Creates a monotonically incrementing unique variable name by prefix. + * + * @param string $prefix Variable name prefix + * + * @return string + */ + private function makeVar($prefix) + { + if (!isset($this->vars[$prefix])) { + $this->vars[$prefix] = 0; + return '$' . $prefix; + } + + return '$' . $prefix . ++$this->vars[$prefix]; + } + + /** + * Writes the given line of source code. Pass positional arguments to write + * that match the format of sprintf. + * + * @param string $str String to write + * @return $this + */ + private function write($str) + { + $this->source .= $this->indentation; + if (func_num_args() == 1) { + $this->source .= $str . "\n"; + return $this; + } + $this->source .= vsprintf($str, array_slice(func_get_args(), 1)) . "\n"; + return $this; + } + + /** + * Decreases the indentation level of code being written + * @return $this + */ + private function outdent() + { + $this->indentation = substr($this->indentation, 0, -4); + return $this; + } + + /** + * Increases the indentation level of code being written + * @return $this + */ + private function indent() + { + $this->indentation .= ' '; + return $this; + } + + private function visit_or(array $node) + { + $a = $this->makeVar('beforeOr'); + return $this + ->write('%s = $value;', $a) + ->dispatch($node['children'][0]) + ->write('if (!$value && $value !== "0" && $value !== 0) {') + ->indent() + ->write('$value = %s;', $a) + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_and(array $node) + { + $a = $this->makeVar('beforeAnd'); + return $this + ->write('%s = $value;', $a) + ->dispatch($node['children'][0]) + ->write('if ($value || $value === "0" || $value === 0) {') + ->indent() + ->write('$value = %s;', $a) + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_not(array $node) + { + return $this + ->write('// Visiting not node') + ->dispatch($node['children'][0]) + ->write('// Applying boolean not to result of not node') + ->write('$value = !Utils::isTruthy($value);'); + } + + private function visit_subexpression(array $node) + { + return $this + ->dispatch($node['children'][0]) + ->write('if ($value !== null) {') + ->indent() + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_field(array $node) + { + $arr = '$value[' . var_export($node['value'], true) . ']'; + $obj = '$value->{' . var_export($node['value'], true) . '}'; + $this->write('if (is_array($value) || $value instanceof \\ArrayAccess) {') + ->indent() + ->write('$value = isset(%s) ? %s : null;', $arr, $arr) + ->outdent() + ->write('} elseif ($value instanceof \\stdClass) {') + ->indent() + ->write('$value = isset(%s) ? %s : null;', $obj, $obj) + ->outdent() + ->write("} else {") + ->indent() + ->write('$value = null;') + ->outdent() + ->write("}"); + + return $this; + } + + private function visit_index(array $node) + { + if ($node['value'] >= 0) { + $check = '$value[' . $node['value'] . ']'; + return $this->write( + '$value = (is_array($value) || $value instanceof \\ArrayAccess)' + . ' && isset(%s) ? %s : null;', + $check, $check + ); + } + + $a = $this->makeVar('count'); + return $this + ->write('if (is_array($value) || ($value instanceof \\ArrayAccess && $value instanceof \\Countable)) {') + ->indent() + ->write('%s = count($value) + %s;', $a, $node['value']) + ->write('$value = isset($value[%s]) ? $value[%s] : null;', $a, $a) + ->outdent() + ->write('} else {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('}'); + } + + private function visit_literal(array $node) + { + return $this->write('$value = %s;', var_export($node['value'], true)); + } + + private function visit_pipe(array $node) + { + return $this + ->dispatch($node['children'][0]) + ->dispatch($node['children'][1]); + } + + private function visit_multi_select_list(array $node) + { + return $this->visit_multi_select_hash($node); + } + + private function visit_multi_select_hash(array $node) + { + $listVal = $this->makeVar('list'); + $value = $this->makeVar('prev'); + $this->write('if ($value !== null) {') + ->indent() + ->write('%s = [];', $listVal) + ->write('%s = $value;', $value); + + $first = true; + foreach ($node['children'] as $child) { + if (!$first) { + $this->write('$value = %s;', $value); + } + $first = false; + if ($node['type'] == 'multi_select_hash') { + $this->dispatch($child['children'][0]); + $key = var_export($child['value'], true); + $this->write('%s[%s] = $value;', $listVal, $key); + } else { + $this->dispatch($child); + $this->write('%s[] = $value;', $listVal); + } + } + + return $this + ->write('$value = %s;', $listVal) + ->outdent() + ->write('}'); + } + + private function visit_function(array $node) + { + $value = $this->makeVar('val'); + $args = $this->makeVar('args'); + $this->write('%s = $value;', $value) + ->write('%s = [];', $args); + + foreach ($node['children'] as $arg) { + $this->dispatch($arg); + $this->write('%s[] = $value;', $args) + ->write('$value = %s;', $value); + } + + return $this->write( + '$value = Fd::getInstance()->__invoke("%s", %s);', + $node['value'], $args + ); + } + + private function visit_slice(array $node) + { + return $this + ->write('$value = !is_string($value) && !Utils::isArray($value)') + ->write(' ? null : Utils::slice($value, %s, %s, %s);', + var_export($node['value'][0], true), + var_export($node['value'][1], true), + var_export($node['value'][2], true) + ); + } + + private function visit_current(array $node) + { + return $this->write('// Visiting current node (no-op)'); + } + + private function visit_expref(array $node) + { + $child = var_export($node['children'][0], true); + return $this->write('$value = function ($value) use ($interpreter) {') + ->indent() + ->write('return $interpreter->visit(%s, $value);', $child) + ->outdent() + ->write('};'); + } + + private function visit_flatten(array $node) + { + $this->dispatch($node['children'][0]); + $merged = $this->makeVar('merged'); + $val = $this->makeVar('val'); + + $this + ->write('// Visiting merge node') + ->write('if (!Utils::isArray($value)) {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('} else {') + ->indent() + ->write('%s = [];', $merged) + ->write('foreach ($value as %s) {', $val) + ->indent() + ->write('if (is_array(%s) && isset(%s[0])) {', $val, $val) + ->indent() + ->write('%s = array_merge(%s, %s);', $merged, $merged, $val) + ->outdent() + ->write('} elseif (%s !== []) {', $val) + ->indent() + ->write('%s[] = %s;', $merged, $val) + ->outdent() + ->write('}') + ->outdent() + ->write('}') + ->write('$value = %s;', $merged) + ->outdent() + ->write('}'); + + return $this; + } + + private function visit_projection(array $node) + { + $val = $this->makeVar('val'); + $collected = $this->makeVar('collected'); + $this->write('// Visiting projection node') + ->dispatch($node['children'][0]) + ->write(''); + + if (!isset($node['from'])) { + $this->write('if (!is_array($value) || !($value instanceof \stdClass)) { $value = null; }'); + } elseif ($node['from'] == 'object') { + $this->write('if (!Utils::isObject($value)) { $value = null; }'); + } elseif ($node['from'] == 'array') { + $this->write('if (!Utils::isArray($value)) { $value = null; }'); + } + + $this->write('if ($value !== null) {') + ->indent() + ->write('%s = [];', $collected) + ->write('foreach ((array) $value as %s) {', $val) + ->indent() + ->write('$value = %s;', $val) + ->dispatch($node['children'][1]) + ->write('if ($value !== null) {') + ->indent() + ->write('%s[] = $value;', $collected) + ->outdent() + ->write('}') + ->outdent() + ->write('}') + ->write('$value = %s;', $collected) + ->outdent() + ->write('}'); + + return $this; + } + + private function visit_condition(array $node) + { + $value = $this->makeVar('beforeCondition'); + return $this + ->write('%s = $value;', $value) + ->write('// Visiting condition node') + ->dispatch($node['children'][0]) + ->write('// Checking result of condition node') + ->write('if (Utils::isTruthy($value)) {') + ->indent() + ->write('$value = %s;', $value) + ->dispatch($node['children'][1]) + ->outdent() + ->write('} else {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('}'); + } + + private function visit_comparator(array $node) + { + $value = $this->makeVar('val'); + $a = $this->makeVar('left'); + $b = $this->makeVar('right'); + + $this + ->write('// Visiting comparator node') + ->write('%s = $value;', $value) + ->dispatch($node['children'][0]) + ->write('%s = $value;', $a) + ->write('$value = %s;', $value) + ->dispatch($node['children'][1]) + ->write('%s = $value;', $b); + + if ($node['value'] == '==') { + $this->write('$value = Utils::isEqual(%s, %s);', $a, $b); + } elseif ($node['value'] == '!=') { + $this->write('$value = !Utils::isEqual(%s, %s);', $a, $b); + } else { + $this->write( + '$value = (is_int(%s) || is_float(%s)) && (is_int(%s) || is_float(%s)) && %s %s %s;', + $a, $a, $b, $b, $a, $node['value'], $b + ); + } + + return $this; + } + + /** @internal */ + public function __call($method, $args) + { + throw new \RuntimeException( + sprintf('Invalid node encountered: %s', json_encode($args[0])) + ); + } +} diff --git a/vendor/mtdowling/jmespath.php/src/TreeInterpreter.php b/vendor/mtdowling/jmespath.php/src/TreeInterpreter.php new file mode 100644 index 0000000..934c506 --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/TreeInterpreter.php @@ -0,0 +1,235 @@ +<?php +namespace JmesPath; + +/** + * Tree visitor used to evaluates JMESPath AST expressions. + */ +class TreeInterpreter +{ + /** @var callable */ + private $fnDispatcher; + + /** + * @param callable|null $fnDispatcher Function dispatching function that accepts + * a function name argument and an array of + * function arguments and returns the result. + */ + public function __construct(callable $fnDispatcher = null) + { + $this->fnDispatcher = $fnDispatcher ?: FnDispatcher::getInstance(); + } + + /** + * Visits each node in a JMESPath AST and returns the evaluated result. + * + * @param array $node JMESPath AST node + * @param mixed $data Data to evaluate + * + * @return mixed + */ + public function visit(array $node, $data) + { + return $this->dispatch($node, $data); + } + + /** + * Recursively traverses an AST using depth-first, pre-order traversal. + * The evaluation logic for each node type is embedded into a large switch + * statement to avoid the cost of "double dispatch". + * @return mixed + */ + private function dispatch(array $node, $value) + { + $dispatcher = $this->fnDispatcher; + + switch ($node['type']) { + + case 'field': + if (is_array($value) || $value instanceof \ArrayAccess) { + return isset($value[$node['value']]) ? $value[$node['value']] : null; + } elseif ($value instanceof \stdClass) { + return isset($value->{$node['value']}) ? $value->{$node['value']} : null; + } + return null; + + case 'subexpression': + return $this->dispatch( + $node['children'][1], + $this->dispatch($node['children'][0], $value) + ); + + case 'index': + if (!Utils::isArray($value)) { + return null; + } + $idx = $node['value'] >= 0 + ? $node['value'] + : $node['value'] + count($value); + return isset($value[$idx]) ? $value[$idx] : null; + + case 'projection': + $left = $this->dispatch($node['children'][0], $value); + switch ($node['from']) { + case 'object': + if (!Utils::isObject($left)) { + return null; + } + break; + case 'array': + if (!Utils::isArray($left)) { + return null; + } + break; + default: + if (!is_array($left) || !($left instanceof \stdClass)) { + return null; + } + } + + $collected = []; + foreach ((array) $left as $val) { + $result = $this->dispatch($node['children'][1], $val); + if ($result !== null) { + $collected[] = $result; + } + } + + return $collected; + + case 'flatten': + static $skipElement = []; + $value = $this->dispatch($node['children'][0], $value); + + if (!Utils::isArray($value)) { + return null; + } + + $merged = []; + foreach ($value as $values) { + // Only merge up arrays lists and not hashes + if (is_array($values) && isset($values[0])) { + $merged = array_merge($merged, $values); + } elseif ($values !== $skipElement) { + $merged[] = $values; + } + } + + return $merged; + + case 'literal': + return $node['value']; + + case 'current': + return $value; + + case 'or': + $result = $this->dispatch($node['children'][0], $value); + return Utils::isTruthy($result) + ? $result + : $this->dispatch($node['children'][1], $value); + + case 'and': + $result = $this->dispatch($node['children'][0], $value); + return Utils::isTruthy($result) + ? $this->dispatch($node['children'][1], $value) + : $result; + + case 'not': + return !Utils::isTruthy( + $this->dispatch($node['children'][0], $value) + ); + + case 'pipe': + return $this->dispatch( + $node['children'][1], + $this->dispatch($node['children'][0], $value) + ); + + case 'multi_select_list': + if ($value === null) { + return null; + } + + $collected = []; + foreach ($node['children'] as $node) { + $collected[] = $this->dispatch($node, $value); + } + + return $collected; + + case 'multi_select_hash': + if ($value === null) { + return null; + } + + $collected = []; + foreach ($node['children'] as $node) { + $collected[$node['value']] = $this->dispatch( + $node['children'][0], + $value + ); + } + + return $collected; + + case 'comparator': + $left = $this->dispatch($node['children'][0], $value); + $right = $this->dispatch($node['children'][1], $value); + if ($node['value'] == '==') { + return Utils::isEqual($left, $right); + } elseif ($node['value'] == '!=') { + return !Utils::isEqual($left, $right); + } else { + return self::relativeCmp($left, $right, $node['value']); + } + + case 'condition': + return Utils::isTruthy($this->dispatch($node['children'][0], $value)) + ? $this->dispatch($node['children'][1], $value) + : null; + + case 'function': + $args = []; + foreach ($node['children'] as $arg) { + $args[] = $this->dispatch($arg, $value); + } + return $dispatcher($node['value'], $args); + + case 'slice': + return is_string($value) || Utils::isArray($value) + ? Utils::slice( + $value, + $node['value'][0], + $node['value'][1], + $node['value'][2] + ) : null; + + case 'expref': + $apply = $node['children'][0]; + return function ($value) use ($apply) { + return $this->visit($apply, $value); + }; + + default: + throw new \RuntimeException("Unknown node type: {$node['type']}"); + } + } + + /** + * @return bool + */ + private static function relativeCmp($left, $right, $cmp) + { + if (!(is_int($left) || is_float($left)) || !(is_int($right) || is_float($right))) { + return false; + } + + switch ($cmp) { + case '>': return $left > $right; + case '>=': return $left >= $right; + case '<': return $left < $right; + case '<=': return $left <= $right; + default: throw new \RuntimeException("Invalid comparison: $cmp"); + } + } +} diff --git a/vendor/mtdowling/jmespath.php/src/Utils.php b/vendor/mtdowling/jmespath.php/src/Utils.php new file mode 100644 index 0000000..9e69fef --- /dev/null +++ b/vendor/mtdowling/jmespath.php/src/Utils.php @@ -0,0 +1,258 @@ +<?php +namespace JmesPath; + +class Utils +{ + public static $typeMap = [ + 'boolean' => 'boolean', + 'string' => 'string', + 'NULL' => 'null', + 'double' => 'number', + 'float' => 'number', + 'integer' => 'number' + ]; + + /** + * Returns true if the value is truthy + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isTruthy($value) + { + if (!$value) { + return $value === 0 || $value === '0'; + } elseif ($value instanceof \stdClass) { + return (bool) get_object_vars($value); + } else { + return true; + } + } + + /** + * Gets the JMESPath type equivalent of a PHP variable. + * + * @param mixed $arg PHP variable + * @return string Returns the JSON data type + * @throws \InvalidArgumentException when an unknown type is given. + */ + public static function type($arg) + { + $type = gettype($arg); + if (isset(self::$typeMap[$type])) { + return self::$typeMap[$type]; + } elseif ($type === 'array') { + if (empty($arg)) { + return 'array'; + } + reset($arg); + return key($arg) === 0 ? 'array' : 'object'; + } elseif ($arg instanceof \stdClass) { + return 'object'; + } elseif ($arg instanceof \Closure) { + return 'expression'; + } elseif ($arg instanceof \ArrayAccess + && $arg instanceof \Countable + ) { + return count($arg) == 0 || $arg->offsetExists(0) + ? 'array' + : 'object'; + } elseif (method_exists($arg, '__toString')) { + return 'string'; + } + + throw new \InvalidArgumentException( + 'Unable to determine JMESPath type from ' . get_class($arg) + ); + } + + /** + * Determine if the provided value is a JMESPath compatible object. + * + * @param mixed $value + * + * @return bool + */ + public static function isObject($value) + { + if (is_array($value)) { + return !$value || array_keys($value)[0] !== 0; + } + + // Handle array-like values. Must be empty or offset 0 does not exist + return $value instanceof \Countable && $value instanceof \ArrayAccess + ? count($value) == 0 || !$value->offsetExists(0) + : $value instanceof \stdClass; + } + + /** + * Determine if the provided value is a JMESPath compatible array. + * + * @param mixed $value + * + * @return bool + */ + public static function isArray($value) + { + if (is_array($value)) { + return !$value || array_keys($value)[0] === 0; + } + + // Handle array-like values. Must be empty or offset 0 exists. + return $value instanceof \Countable && $value instanceof \ArrayAccess + ? count($value) == 0 || $value->offsetExists(0) + : false; + } + + /** + * JSON aware value comparison function. + * + * @param mixed $a First value to compare + * @param mixed $b Second value to compare + * + * @return bool + */ + public static function isEqual($a, $b) + { + if ($a === $b) { + return true; + } elseif ($a instanceof \stdClass) { + return self::isEqual((array) $a, $b); + } elseif ($b instanceof \stdClass) { + return self::isEqual($a, (array) $b); + } else { + return false; + } + } + + /** + * Safely add together two values. + * + * @param mixed $a First value to add + * @param mixed $b Second value to add + * + * @return int|float + */ + public static function add($a, $b) + { + if (is_numeric($a)) { + if (is_numeric($b)) { + return $a + $b; + } else { + return $a; + } + } else { + if (is_numeric($b)) { + return $b; + } else { + return 0; + } + } + } + + /** + * JMESPath requires a stable sorting algorithm, so here we'll implement + * a simple Schwartzian transform that uses array index positions as tie + * breakers. + * + * @param array $data List or map of data to sort + * @param callable $sortFn Callable used to sort values + * + * @return array Returns the sorted array + * @link http://en.wikipedia.org/wiki/Schwartzian_transform + */ + public static function stableSort(array $data, callable $sortFn) + { + // Decorate each item by creating an array of [value, index] + array_walk($data, function (&$v, $k) { + $v = [$v, $k]; + }); + // Sort by the sort function and use the index as a tie-breaker + uasort($data, function ($a, $b) use ($sortFn) { + return $sortFn($a[0], $b[0]) ?: ($a[1] < $b[1] ? -1 : 1); + }); + + // Undecorate each item and return the resulting sorted array + return array_map(function ($v) { + return $v[0]; + }, array_values($data)); + } + + /** + * Creates a Python-style slice of a string or array. + * + * @param array|string $value Value to slice + * @param int|null $start Starting position + * @param int|null $stop Stop position + * @param int $step Step (1, 2, -1, -2, etc.) + * + * @return array|string + * @throws \InvalidArgumentException + */ + public static function slice($value, $start = null, $stop = null, $step = 1) + { + if (!is_array($value) && !is_string($value)) { + throw new \InvalidArgumentException('Expects string or array'); + } + + return self::sliceIndices($value, $start, $stop, $step); + } + + private static function adjustEndpoint($length, $endpoint, $step) + { + if ($endpoint < 0) { + $endpoint += $length; + if ($endpoint < 0) { + $endpoint = $step < 0 ? -1 : 0; + } + } elseif ($endpoint >= $length) { + $endpoint = $step < 0 ? $length - 1 : $length; + } + + return $endpoint; + } + + private static function adjustSlice($length, $start, $stop, $step) + { + if ($step === null) { + $step = 1; + } elseif ($step === 0) { + throw new \RuntimeException('step cannot be 0'); + } + + if ($start === null) { + $start = $step < 0 ? $length - 1 : 0; + } else { + $start = self::adjustEndpoint($length, $start, $step); + } + + if ($stop === null) { + $stop = $step < 0 ? -1 : $length; + } else { + $stop = self::adjustEndpoint($length, $stop, $step); + } + + return [$start, $stop, $step]; + } + + private static function sliceIndices($subject, $start, $stop, $step) + { + $type = gettype($subject); + $len = $type == 'string' ? mb_strlen($subject, 'UTF-8') : count($subject); + list($start, $stop, $step) = self::adjustSlice($len, $start, $stop, $step); + + $result = []; + if ($step > 0) { + for ($i = $start; $i < $stop; $i += $step) { + $result[] = $subject[$i]; + } + } else { + for ($i = $start; $i > $stop; $i += $step) { + $result[] = $subject[$i]; + } + } + + return $type == 'string' ? implode('', $result) : $result; + } +} |