FnDispatcher::fn_map()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
1
<?php
2
namespace JmesPath;
3
4
/**
5
 * Dispatches to named JMESPath functions using a single function that has the
6
 * following signature:
7
 *
8
 *     mixed $result = fn(string $function_name, array $args)
9
 */
10
class FnDispatcher
11
{
12
    /**
13
     * Gets a cached instance of the default function implementations.
14
     *
15
     * @return FnDispatcher
16
     */
17
    public static function getInstance()
18
    {
19
        static $instance = null;
20
        if (!$instance) {
21
            $instance = new self();
22
        }
23
24
        return $instance;
25
    }
26
27
    /**
28
     * @param string $fn   Function name.
29
     * @param array  $args Function arguments.
30
     *
31
     * @return mixed
32
     */
33
    public function __invoke($fn, array $args)
34
    {
35
        return $this->{'fn_' . $fn}($args);
36
    }
37
38
    private function fn_abs(array $args)
39
    {
40
        $this->validate('abs', $args, [['number']]);
41
        return abs($args[0]);
42
    }
43
44
    private function fn_avg(array $args)
45
    {
46
        $this->validate('avg', $args, [['array']]);
47
        $sum = $this->reduce('avg:0', $args[0], ['number'], function ($a, $b) {
48
            return $a + $b;
49
        });
50
        return $args[0] ? ($sum / count($args[0])) : null;
51
    }
52
53
    private function fn_ceil(array $args)
54
    {
55
        $this->validate('ceil', $args, [['number']]);
56
        return ceil($args[0]);
57
    }
58
59
    private function fn_contains(array $args)
60
    {
61
        $this->validate('contains', $args, [['string', 'array'], ['any']]);
62
        if (is_array($args[0])) {
63
            return in_array($args[1], $args[0]);
64
        } elseif (is_string($args[1])) {
65
            return strpos($args[0], $args[1]) !== false;
66
        } else {
67
            return null;
68
        }
69
    }
70
71
    private function fn_ends_with(array $args)
72
    {
73
        $this->validate('ends_with', $args, [['string'], ['string']]);
74
        list($search, $suffix) = $args;
75
        return $suffix === '' || substr($search, -strlen($suffix)) === $suffix;
76
    }
77
78
    private function fn_floor(array $args)
79
    {
80
        $this->validate('floor', $args, [['number']]);
81
        return floor($args[0]);
82
    }
83
84
    private function fn_not_null(array $args)
85
    {
86
        if (!$args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
87
            throw new \RuntimeException(
88
                "not_null() expects 1 or more arguments, 0 were provided"
89
            );
90
        }
91
92
        return array_reduce($args, function ($carry, $item) {
93
            return $carry !== null ? $carry : $item;
94
        });
95
    }
96
97
    private function fn_join(array $args)
98
    {
99
        $this->validate('join', $args, [['string'], ['array']]);
100
        $fn = function ($a, $b, $i) use ($args) {
101
            return $i ? ($a . $args[0] . $b) : $b;
102
        };
103
        return $this->reduce('join:0', $args[1], ['string'], $fn);
104
    }
105
106
    private function fn_keys(array $args)
107
    {
108
        $this->validate('keys', $args, [['object']]);
109
        return array_keys((array) $args[0]);
110
    }
111
112
    private function fn_length(array $args)
113
    {
114
        $this->validate('length', $args, [['string', 'array', 'object']]);
115
        return is_string($args[0]) ? strlen($args[0]) : count((array) $args[0]);
116
    }
117
118
    private function fn_max(array $args)
119
    {
120
        $this->validate('max', $args, [['array']]);
121
        $fn = function ($a, $b) { return $a >= $b ? $a : $b; };
122
        return $this->reduce('max:0', $args[0], ['number', 'string'], $fn);
123
    }
124
125
    private function fn_max_by(array $args)
126
    {
127
        $this->validate('max_by', $args, [['array'], ['expression']]);
128
        $expr = $this->wrapExpression('max_by:1', $args[1], ['number', 'string']);
129
        $fn = function ($carry, $item, $index) use ($expr) {
130
            return $index
131
                ? ($expr($carry) >= $expr($item) ? $carry : $item)
132
                : $item;
133
        };
134
        return $this->reduce('max_by:1', $args[0], ['any'], $fn);
135
    }
136
137
    private function fn_min(array $args)
138
    {
139
        $this->validate('min', $args, [['array']]);
140
        $fn = function ($a, $b, $i) { return $i && $a <= $b ? $a : $b; };
141
        return $this->reduce('min:0', $args[0], ['number', 'string'], $fn);
142
    }
143
144
    private function fn_min_by(array $args)
145
    {
146
        $this->validate('min_by', $args, [['array'], ['expression']]);
147
        $expr = $this->wrapExpression('min_by:1', $args[1], ['number', 'string']);
148
        $i = -1;
149
        $fn = function ($a, $b) use ($expr, &$i) {
150
            return ++$i ? ($expr($a) <= $expr($b) ? $a : $b) : $b;
151
        };
152
        return $this->reduce('min_by:1', $args[0], ['any'], $fn);
153
    }
154
155
    private function fn_reverse(array $args)
156
    {
157
        $this->validate('reverse', $args, [['array', 'string']]);
158
        if (is_array($args[0])) {
159
            return array_reverse($args[0]);
160
        } elseif (is_string($args[0])) {
161
            return strrev($args[0]);
162
        } else {
163
            throw new \RuntimeException('Cannot reverse provided argument');
164
        }
165
    }
166
167
    private function fn_sum(array $args)
168
    {
169
        $this->validate('sum', $args, [['array']]);
170
        $fn = function ($a, $b) { return $a + $b; };
171
        return $this->reduce('sum:0', $args[0], ['number'], $fn);
172
    }
173
174
    private function fn_sort(array $args)
175
    {
176
        $this->validate('sort', $args, [['array']]);
177
        $valid = ['string', 'number'];
178
        return Utils::stableSort($args[0], function ($a, $b) use ($valid) {
179
            $this->validateSeq('sort:0', $valid, $a, $b);
180
            return strnatcmp($a, $b);
181
        });
182
    }
183
184
    private function fn_sort_by(array $args)
185
    {
186
        $this->validate('sort_by', $args, [['array'], ['expression']]);
187
        $expr = $args[1];
188
        $valid = ['string', 'number'];
189
        return Utils::stableSort(
190
            $args[0],
191
            function ($a, $b) use ($expr, $valid) {
192
                $va = $expr($a);
193
                $vb = $expr($b);
194
                $this->validateSeq('sort_by:0', $valid, $va, $vb);
195
                return strnatcmp($va, $vb);
196
            }
197
        );
198
    }
199
200
    private function fn_starts_with(array $args)
201
    {
202
        $this->validate('starts_with', $args, [['string'], ['string']]);
203
        list($search, $prefix) = $args;
204
        return $prefix === '' || strpos($search, $prefix) === 0;
205
    }
206
207
    private function fn_type(array $args)
208
    {
209
        $this->validateArity('type', count($args), 1);
210
        return Utils::type($args[0]);
211
    }
212
213
    private function fn_to_string(array $args)
214
    {
215
        $this->validateArity('to_string', count($args), 1);
216
        $v = $args[0];
217
        if (is_string($v)) {
218
            return $v;
219
        } elseif (is_object($v)
220
            && !($v instanceof \JsonSerializable)
221
            && method_exists($v, '__toString')
222
        ) {
223
            return (string) $v;
224
        }
225
226
        return json_encode($v);
227
    }
228
229
    private function fn_to_number(array $args)
230
    {
231
        $this->validateArity('to_number', count($args), 1);
232
        $value = $args[0];
233
        $type = Utils::type($value);
234
        if ($type == 'number') {
235
            return $value;
236
        } elseif ($type == 'string' && is_numeric($value)) {
237
            return strpos($value, '.') ? (float) $value : (int) $value;
238
        } else {
239
            return null;
240
        }
241
    }
242
243
    private function fn_values(array $args)
244
    {
245
        $this->validate('values', $args, [['array', 'object']]);
246
        return array_values((array) $args[0]);
247
    }
248
249
    private function fn_merge(array $args)
250
    {
251
        if (!$args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
252
            throw new \RuntimeException(
253
                "merge() expects 1 or more arguments, 0 were provided"
254
            );
255
        }
256
257
        return call_user_func_array('array_replace', $args);
258
    }
259
260
    private function fn_to_array(array $args)
261
    {
262
        $this->validate('to_array', $args, [['any']]);
263
264
        return Utils::isArray($args[0]) ? $args[0] : [$args[0]];
265
    }
266
267
    private function fn_map(array $args)
268
    {
269
        $this->validate('map', $args, [['expression'], ['any']]);
270
        $result = [];
271
        foreach ($args[1] as $a) {
272
            $result[] = $args[0]($a);
273
        }
274
        return $result;
275
    }
276
277
    private function typeError($from, $msg)
278
    {
279
        if (strpos($from, ':')) {
280
            list($fn, $pos) = explode(':', $from);
281
            throw new \RuntimeException(
282
                sprintf('Argument %d of %s %s', $pos, $fn, $msg)
283
            );
284
        } else {
285
            throw new \RuntimeException(
286
                sprintf('Type error: %s %s', $from, $msg)
287
            );
288
        }
289
    }
290
291
    private function validateArity($from, $given, $expected)
292
    {
293
        if ($given != $expected) {
294
            $err = "%s() expects {$expected} arguments, {$given} were provided";
295
            throw new \RuntimeException(sprintf($err, $from));
296
        }
297
    }
298
299
    private function validate($from, $args, $types = [])
300
    {
301
        $this->validateArity($from, count($args), count($types));
302
        foreach ($args as $index => $value) {
303
            if (!isset($types[$index]) || !$types[$index]) {
304
                continue;
305
            }
306
            $this->validateType("{$from}:{$index}", $value, $types[$index]);
307
        }
308
    }
309
310
    private function validateType($from, $value, array $types)
311
    {
312
        if ($types[0] == 'any'
313
            || in_array(Utils::type($value), $types)
314
            || ($value === [] && in_array('object', $types))
315
        ) {
316
            return;
317
        }
318
        $msg = 'must be one of the following types: ' . implode(', ', $types)
319
            . '. ' . Utils::type($value) . ' found';
320
        $this->typeError($from, $msg);
321
    }
322
323
    /**
324
     * Validates value A and B, ensures they both are correctly typed, and of
325
     * the same type.
326
     *
327
     * @param string   $from   String of function:argument_position
328
     * @param array    $types  Array of valid value types.
329
     * @param mixed    $a      Value A
330
     * @param mixed    $b      Value B
331
     */
332
    private function validateSeq($from, array $types, $a, $b)
333
    {
334
        $ta = Utils::type($a);
335
        $tb = Utils::type($b);
336
337
        if ($ta !== $tb) {
338
            $msg = "encountered a type mismatch in sequence: {$ta}, {$tb}";
339
            $this->typeError($from, $msg);
340
        }
341
342
        $typeMatch = ($types && $types[0] == 'any') || in_array($ta, $types);
0 ignored issues
show
Bug Best Practice introduced by
The expression $types of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
343
        if (!$typeMatch) {
344
            $msg = 'encountered a type error in sequence. The argument must be '
345
                . 'an array of ' . implode('|', $types) . ' types. '
346
                . "Found {$ta}, {$tb}.";
347
            $this->typeError($from, $msg);
348
        }
349
    }
350
351
    /**
352
     * Reduces and validates an array of values to a single value using a fn.
353
     *
354
     * @param string   $from   String of function:argument_position
355
     * @param array    $values Values to reduce.
356
     * @param array    $types  Array of valid value types.
357
     * @param callable $reduce Reduce function that accepts ($carry, $item).
358
     *
359
     * @return mixed
360
     */
361
    private function reduce($from, array $values, array $types, callable $reduce)
362
    {
363
        $i = -1;
364
        return array_reduce(
365
            $values,
366
            function ($carry, $item) use ($from, $types, $reduce, &$i) {
367
                if (++$i > 0) {
368
                    $this->validateSeq($from, $types, $carry, $item);
369
                }
370
                return $reduce($carry, $item, $i);
371
            }
372
        );
373
    }
374
375
    /**
376
     * Validates the return values of expressions as they are applied.
377
     *
378
     * @param string   $from  Function name : position
379
     * @param callable $expr  Expression function to validate.
380
     * @param array    $types Array of acceptable return type values.
381
     *
382
     * @return callable Returns a wrapped function
383
     */
384
    private function wrapExpression($from, callable $expr, array $types)
385
    {
386
        list($fn, $pos) = explode(':', $from);
387
        $from = "The expression return value of argument {$pos} of {$fn}";
388
        return function ($value) use ($from, $expr, $types) {
389
            $value = $expr($value);
390
            $this->validateType($from, $value, $types);
391
            return $value;
392
        };
393
    }
394
395
    /** @internal Pass function name validation off to runtime */
396
    public function __call($name, $args)
397
    {
398
        $name = str_replace('fn_', '', $name);
399
        throw new \RuntimeException("Call to undefined function {$name}");
400
    }
401
}
402