mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 20:42:22 +02:00
MDL-78511 lib: Include JmesPath library
JMESPath library is required by AWS SDK for PHP library
This commit is contained in:
parent
9b98f78d2a
commit
f81d6ed426
@ -122,6 +122,7 @@ class core_component {
|
||||
'GuzzleHttp' => 'lib/guzzlehttp/guzzle/src',
|
||||
'Kevinrob\\GuzzleCache' => 'lib/guzzlehttp/kevinrob/guzzlecache/src',
|
||||
'Aws' => 'lib/aws-sdk/src',
|
||||
'JmesPath' => 'lib/jmespath/src',
|
||||
];
|
||||
|
||||
/**
|
||||
|
62
lib/jmespath/CHANGELOG.md
Normal file
62
lib/jmespath/CHANGELOG.md
Normal file
@ -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.
|
19
lib/jmespath/LICENSE
Normal file
19
lib/jmespath/LICENSE
Normal file
@ -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.
|
123
lib/jmespath/README.rst
Normal file
123
lib/jmespath/README.rst
Normal file
@ -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
|
39
lib/jmespath/composer.json
Normal file
39
lib/jmespath/composer.json
Normal file
@ -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": "mtdowling@gmail.com",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
6
lib/jmespath/readme_moodle.txt
Normal file
6
lib/jmespath/readme_moodle.txt
Normal file
@ -0,0 +1,6 @@
|
||||
Instructions to import/update jmespath library into Moodle:
|
||||
|
||||
Update jmespath library
|
||||
1. Download the latest jmespath.php library package from https://github.com/jmespath/jmespath.php/releases
|
||||
2. Copy the src directory to lib/jmespath/src folder
|
||||
3. Copy the associated files LICENCE, README.md etc. to jmespath directory
|
47
lib/jmespath/src/AstRuntime.php
Normal file
47
lib/jmespath/src/AstRuntime.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
83
lib/jmespath/src/CompilerRuntime.php
Normal file
83
lib/jmespath/src/CompilerRuntime.php
Normal file
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
109
lib/jmespath/src/DebugRuntime.php
Normal file
109
lib/jmespath/src/DebugRuntime.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
91
lib/jmespath/src/Env.php
Normal file
91
lib/jmespath/src/Env.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
407
lib/jmespath/src/FnDispatcher.php
Normal file
407
lib/jmespath/src/FnDispatcher.php
Normal file
@ -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}");
|
||||
}
|
||||
}
|
17
lib/jmespath/src/JmesPath.php
Normal file
17
lib/jmespath/src/JmesPath.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
444
lib/jmespath/src/Lexer.php
Normal file
444
lib/jmespath/src/Lexer.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
519
lib/jmespath/src/Parser.php
Normal file
519
lib/jmespath/src/Parser.php
Normal file
@ -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");
|
||||
}
|
||||
}
|
36
lib/jmespath/src/SyntaxErrorException.php
Normal file
36
lib/jmespath/src/SyntaxErrorException.php
Normal file
@ -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']
|
||||
);
|
||||
}
|
||||
}
|
419
lib/jmespath/src/TreeCompiler.php
Normal file
419
lib/jmespath/src/TreeCompiler.php
Normal file
@ -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]))
|
||||
);
|
||||
}
|
||||
}
|
235
lib/jmespath/src/TreeInterpreter.php
Normal file
235
lib/jmespath/src/TreeInterpreter.php
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
258
lib/jmespath/src/Utils.php
Normal file
258
lib/jmespath/src/Utils.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -725,4 +725,15 @@ All rights reserved.</copyright>
|
||||
<licenseversion>2.0</licenseversion>
|
||||
<repository>https://github.com/aws/aws-sdk-php</repository>
|
||||
</library>
|
||||
<library>
|
||||
<location>jmespath</location>
|
||||
<name>JMESPath</name>
|
||||
<description>The JMESPath library</description>
|
||||
<version>2.6.1</version>
|
||||
<license>MIT</license>
|
||||
<repository>https://github.com/jmespath/jmespath.php</repository>
|
||||
<copyrights>
|
||||
<copyright>2014 Michael Dowling, https://github.com/mtdowling</copyright>
|
||||
</copyrights>
|
||||
</library>
|
||||
</libraries>
|
||||
|
Loading…
x
Reference in New Issue
Block a user