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