123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- <?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");
- }
- }
- }
|