FnDispatcher.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <?php
  2. namespace JmesPath;
  3. /**
  4. * Dispatches to named JMESPath functions using a single function that has the
  5. * following signature:
  6. *
  7. * mixed $result = fn(string $function_name, array $args)
  8. */
  9. class FnDispatcher
  10. {
  11. /**
  12. * Gets a cached instance of the default function implementations.
  13. *
  14. * @return FnDispatcher
  15. */
  16. public static function getInstance()
  17. {
  18. static $instance = null;
  19. if (!$instance) {
  20. $instance = new self();
  21. }
  22. return $instance;
  23. }
  24. /**
  25. * @param string $fn Function name.
  26. * @param array $args Function arguments.
  27. *
  28. * @return mixed
  29. */
  30. public function __invoke($fn, array $args)
  31. {
  32. return $this->{'fn_' . $fn}($args);
  33. }
  34. private function fn_abs(array $args)
  35. {
  36. $this->validate('abs', $args, [['number']]);
  37. return abs($args[0]);
  38. }
  39. private function fn_avg(array $args)
  40. {
  41. $this->validate('avg', $args, [['array']]);
  42. $sum = $this->reduce('avg:0', $args[0], ['number'], function ($a, $b) {
  43. return $a + $b;
  44. });
  45. return $args[0] ? ($sum / count($args[0])) : null;
  46. }
  47. private function fn_ceil(array $args)
  48. {
  49. $this->validate('ceil', $args, [['number']]);
  50. return ceil($args[0]);
  51. }
  52. private function fn_contains(array $args)
  53. {
  54. $this->validate('contains', $args, [['string', 'array'], ['any']]);
  55. if (is_array($args[0])) {
  56. return in_array($args[1], $args[0]);
  57. } elseif (is_string($args[1])) {
  58. return strpos($args[0], $args[1]) !== false;
  59. } else {
  60. return null;
  61. }
  62. }
  63. private function fn_ends_with(array $args)
  64. {
  65. $this->validate('ends_with', $args, [['string'], ['string']]);
  66. list($search, $suffix) = $args;
  67. return $suffix === '' || substr($search, -strlen($suffix)) === $suffix;
  68. }
  69. private function fn_floor(array $args)
  70. {
  71. $this->validate('floor', $args, [['number']]);
  72. return floor($args[0]);
  73. }
  74. private function fn_not_null(array $args)
  75. {
  76. if (!$args) {
  77. throw new \RuntimeException(
  78. "not_null() expects 1 or more arguments, 0 were provided"
  79. );
  80. }
  81. return array_reduce($args, function ($carry, $item) {
  82. return $carry !== null ? $carry : $item;
  83. });
  84. }
  85. private function fn_join(array $args)
  86. {
  87. $this->validate('join', $args, [['string'], ['array']]);
  88. $fn = function ($a, $b, $i) use ($args) {
  89. return $i ? ($a . $args[0] . $b) : $b;
  90. };
  91. return $this->reduce('join:0', $args[1], ['string'], $fn);
  92. }
  93. private function fn_keys(array $args)
  94. {
  95. $this->validate('keys', $args, [['object']]);
  96. return array_keys((array) $args[0]);
  97. }
  98. private function fn_length(array $args)
  99. {
  100. $this->validate('length', $args, [['string', 'array', 'object']]);
  101. return is_string($args[0]) ? strlen($args[0]) : count((array) $args[0]);
  102. }
  103. private function fn_max(array $args)
  104. {
  105. $this->validate('max', $args, [['array']]);
  106. $fn = function ($a, $b) { return $a >= $b ? $a : $b; };
  107. return $this->reduce('max:0', $args[0], ['number', 'string'], $fn);
  108. }
  109. private function fn_max_by(array $args)
  110. {
  111. $this->validate('max_by', $args, [['array'], ['expression']]);
  112. $expr = $this->wrapExpression('max_by:1', $args[1], ['number', 'string']);
  113. $fn = function ($carry, $item, $index) use ($expr) {
  114. return $index
  115. ? ($expr($carry) >= $expr($item) ? $carry : $item)
  116. : $item;
  117. };
  118. return $this->reduce('max_by:1', $args[0], ['any'], $fn);
  119. }
  120. private function fn_min(array $args)
  121. {
  122. $this->validate('min', $args, [['array']]);
  123. $fn = function ($a, $b, $i) { return $i && $a <= $b ? $a : $b; };
  124. return $this->reduce('min:0', $args[0], ['number', 'string'], $fn);
  125. }
  126. private function fn_min_by(array $args)
  127. {
  128. $this->validate('min_by', $args, [['array'], ['expression']]);
  129. $expr = $this->wrapExpression('min_by:1', $args[1], ['number', 'string']);
  130. $i = -1;
  131. $fn = function ($a, $b) use ($expr, &$i) {
  132. return ++$i ? ($expr($a) <= $expr($b) ? $a : $b) : $b;
  133. };
  134. return $this->reduce('min_by:1', $args[0], ['any'], $fn);
  135. }
  136. private function fn_reverse(array $args)
  137. {
  138. $this->validate('reverse', $args, [['array', 'string']]);
  139. if (is_array($args[0])) {
  140. return array_reverse($args[0]);
  141. } elseif (is_string($args[0])) {
  142. return strrev($args[0]);
  143. } else {
  144. throw new \RuntimeException('Cannot reverse provided argument');
  145. }
  146. }
  147. private function fn_sum(array $args)
  148. {
  149. $this->validate('sum', $args, [['array']]);
  150. $fn = function ($a, $b) { return $a + $b; };
  151. return $this->reduce('sum:0', $args[0], ['number'], $fn);
  152. }
  153. private function fn_sort(array $args)
  154. {
  155. $this->validate('sort', $args, [['array']]);
  156. $valid = ['string', 'number'];
  157. return Utils::stableSort($args[0], function ($a, $b) use ($valid) {
  158. $this->validateSeq('sort:0', $valid, $a, $b);
  159. return strnatcmp($a, $b);
  160. });
  161. }
  162. private function fn_sort_by(array $args)
  163. {
  164. $this->validate('sort_by', $args, [['array'], ['expression']]);
  165. $expr = $args[1];
  166. $valid = ['string', 'number'];
  167. return Utils::stableSort(
  168. $args[0],
  169. function ($a, $b) use ($expr, $valid) {
  170. $va = $expr($a);
  171. $vb = $expr($b);
  172. $this->validateSeq('sort_by:0', $valid, $va, $vb);
  173. return strnatcmp($va, $vb);
  174. }
  175. );
  176. }
  177. private function fn_starts_with(array $args)
  178. {
  179. $this->validate('starts_with', $args, [['string'], ['string']]);
  180. list($search, $prefix) = $args;
  181. return $prefix === '' || strpos($search, $prefix) === 0;
  182. }
  183. private function fn_type(array $args)
  184. {
  185. $this->validateArity('type', count($args), 1);
  186. return Utils::type($args[0]);
  187. }
  188. private function fn_to_string(array $args)
  189. {
  190. $this->validateArity('to_string', count($args), 1);
  191. $v = $args[0];
  192. if (is_string($v)) {
  193. return $v;
  194. } elseif (is_object($v)
  195. && !($v instanceof \JsonSerializable)
  196. && method_exists($v, '__toString')
  197. ) {
  198. return (string) $v;
  199. }
  200. return json_encode($v);
  201. }
  202. private function fn_to_number(array $args)
  203. {
  204. $this->validateArity('to_number', count($args), 1);
  205. $value = $args[0];
  206. $type = Utils::type($value);
  207. if ($type == 'number') {
  208. return $value;
  209. } elseif ($type == 'string' && is_numeric($value)) {
  210. return strpos($value, '.') ? (float) $value : (int) $value;
  211. } else {
  212. return null;
  213. }
  214. }
  215. private function fn_values(array $args)
  216. {
  217. $this->validate('values', $args, [['array', 'object']]);
  218. return array_values((array) $args[0]);
  219. }
  220. private function fn_merge(array $args)
  221. {
  222. if (!$args) {
  223. throw new \RuntimeException(
  224. "merge() expects 1 or more arguments, 0 were provided"
  225. );
  226. }
  227. return call_user_func_array('array_replace', $args);
  228. }
  229. private function fn_to_array(array $args)
  230. {
  231. $this->validate('to_array', $args, [['any']]);
  232. return Utils::isArray($args[0]) ? $args[0] : [$args[0]];
  233. }
  234. private function fn_map(array $args)
  235. {
  236. $this->validate('map', $args, [['expression'], ['any']]);
  237. $result = [];
  238. foreach ($args[1] as $a) {
  239. $result[] = $args[0]($a);
  240. }
  241. return $result;
  242. }
  243. private function typeError($from, $msg)
  244. {
  245. if (strpos($from, ':')) {
  246. list($fn, $pos) = explode(':', $from);
  247. throw new \RuntimeException(
  248. sprintf('Argument %d of %s %s', $pos, $fn, $msg)
  249. );
  250. } else {
  251. throw new \RuntimeException(
  252. sprintf('Type error: %s %s', $from, $msg)
  253. );
  254. }
  255. }
  256. private function validateArity($from, $given, $expected)
  257. {
  258. if ($given != $expected) {
  259. $err = "%s() expects {$expected} arguments, {$given} were provided";
  260. throw new \RuntimeException(sprintf($err, $from));
  261. }
  262. }
  263. private function validate($from, $args, $types = [])
  264. {
  265. $this->validateArity($from, count($args), count($types));
  266. foreach ($args as $index => $value) {
  267. if (!isset($types[$index]) || !$types[$index]) {
  268. continue;
  269. }
  270. $this->validateType("{$from}:{$index}", $value, $types[$index]);
  271. }
  272. }
  273. private function validateType($from, $value, array $types)
  274. {
  275. if ($types[0] == 'any'
  276. || in_array(Utils::type($value), $types)
  277. || ($value === [] && in_array('object', $types))
  278. ) {
  279. return;
  280. }
  281. $msg = 'must be one of the following types: ' . implode(', ', $types)
  282. . '. ' . Utils::type($value) . ' found';
  283. $this->typeError($from, $msg);
  284. }
  285. /**
  286. * Validates value A and B, ensures they both are correctly typed, and of
  287. * the same type.
  288. *
  289. * @param string $from String of function:argument_position
  290. * @param array $types Array of valid value types.
  291. * @param mixed $a Value A
  292. * @param mixed $b Value B
  293. */
  294. private function validateSeq($from, array $types, $a, $b)
  295. {
  296. $ta = Utils::type($a);
  297. $tb = Utils::type($b);
  298. if ($ta !== $tb) {
  299. $msg = "encountered a type mismatch in sequence: {$ta}, {$tb}";
  300. $this->typeError($from, $msg);
  301. }
  302. $typeMatch = ($types && $types[0] == 'any') || in_array($ta, $types);
  303. if (!$typeMatch) {
  304. $msg = 'encountered a type error in sequence. The argument must be '
  305. . 'an array of ' . implode('|', $types) . ' types. '
  306. . "Found {$ta}, {$tb}.";
  307. $this->typeError($from, $msg);
  308. }
  309. }
  310. /**
  311. * Reduces and validates an array of values to a single value using a fn.
  312. *
  313. * @param string $from String of function:argument_position
  314. * @param array $values Values to reduce.
  315. * @param array $types Array of valid value types.
  316. * @param callable $reduce Reduce function that accepts ($carry, $item).
  317. *
  318. * @return mixed
  319. */
  320. private function reduce($from, array $values, array $types, callable $reduce)
  321. {
  322. $i = -1;
  323. return array_reduce(
  324. $values,
  325. function ($carry, $item) use ($from, $types, $reduce, &$i) {
  326. if (++$i > 0) {
  327. $this->validateSeq($from, $types, $carry, $item);
  328. }
  329. return $reduce($carry, $item, $i);
  330. }
  331. );
  332. }
  333. /**
  334. * Validates the return values of expressions as they are applied.
  335. *
  336. * @param string $from Function name : position
  337. * @param callable $expr Expression function to validate.
  338. * @param array $types Array of acceptable return type values.
  339. *
  340. * @return callable Returns a wrapped function
  341. */
  342. private function wrapExpression($from, callable $expr, array $types)
  343. {
  344. list($fn, $pos) = explode(':', $from);
  345. $from = "The expression return value of argument {$pos} of {$fn}";
  346. return function ($value) use ($from, $expr, $types) {
  347. $value = $expr($value);
  348. $this->validateType($from, $value, $types);
  349. return $value;
  350. };
  351. }
  352. /** @internal Pass function name validation off to runtime */
  353. public function __call($name, $args)
  354. {
  355. $name = str_replace('fn_', '', $name);
  356. throw new \RuntimeException("Call to undefined function {$name}");
  357. }
  358. }