spaces.php

This commit is contained in:
Devang Srivastava 2017-12-07 21:23:18 +05:30
commit eefa32741e
845 changed files with 50409 additions and 0 deletions

View 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|null 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);
}
}

View 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 $dir Directory used to store compiled PHP files.
* @param Parser $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|null 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)
));
}
}
}

View 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;
}
}

66
aws/JmesPath/Env.php Normal file
View file

@ -0,0 +1,66 @@
<?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|null 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 = getenv(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 = getenv(self::COMPILE_DIR) ?: sys_get_temp_dir();
foreach (glob("{$compileDir}/jmespath_*.php") as $file) {
$total++;
unlink($file);
}
return $total;
}
}

View file

@ -0,0 +1,401 @@
<?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 $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 strpos($args[0], $args[1]) !== false;
} else {
return null;
}
}
private function fn_ends_with(array $args)
{
$this->validate('ends_with', $args, [['string'], ['string']]);
list($search, $suffix) = $args;
return $suffix === '' || substr($search, -strlen($suffix)) === $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]) ? strlen($args[0]) : 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 $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 === '' || strpos($search, $prefix) === 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 strpos($value, '.') ? (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 (strpos($from, ':')) {
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
aws/JmesPath/JmesPath.php Normal file
View 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|null
*/
if (!function_exists(__NAMESPACE__ . '\search')) {
function search($expression, $data)
{
return Env::search($expression, $data);
}
}

444
aws/JmesPath/Lexer.php Normal file
View 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' => strlen($input),
'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;
}
}

518
aws/JmesPath/Parser.php Normal file
View file

@ -0,0 +1,518 @@
<?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 $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");
}
}

View 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(' ', $token['pos']) . "^\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']
);
}
}

View 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 Fn;')
->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 = Fn::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]))
);
}
}

View file

@ -0,0 +1,235 @@
<?php
namespace JmesPath;
/**
* Tree visitor used to evaluates JMESPath AST expressions.
*/
class TreeInterpreter
{
/** @var callable */
private $fnDispatcher;
/**
* @param callable $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");
}
}
}

229
aws/JmesPath/Utils.php Normal file
View file

@ -0,0 +1,229 @@
<?php
namespace JmesPath;
class Utils
{
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;
}
}
/**
* 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' ? strlen($subject) : 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;
}
}