| 
<?phpnamespace 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_int($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");
 }
 }
 }
 
 |