JSONPathTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <?php
  2. namespace Flow\JSONPath\Test;
  3. require_once __DIR__ . "/../vendor/autoload.php";
  4. use Flow\JSONPath\JSONPath;
  5. use PHPUnit\Framework\TestCase;
  6. class JSONPathTest extends TestCase
  7. {
  8. /**
  9. * $.store.books[0].title
  10. */
  11. public function testChildOperators()
  12. {
  13. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find('$.store.books[0].title');
  14. $this->assertEquals('Sayings of the Century', $result[0]);
  15. }
  16. /**
  17. * $['store']['books'][0]['title']
  18. */
  19. public function testChildOperatorsAlt()
  20. {
  21. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][0]['title']");
  22. $this->assertEquals('Sayings of the Century', $result[0]);
  23. }
  24. /**
  25. * $.array[start:end:step]
  26. */
  27. public function testFilterSliceA()
  28. {
  29. // Copy all items... similar to a wildcard
  30. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$['store']['books'][:].title");
  31. $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick', 'The Lord of the Rings'], $result->data());
  32. }
  33. /**
  34. * Positive end indexes
  35. * $[0:2]
  36. */
  37. public function testFilterSlice_PositiveEndIndexes()
  38. {
  39. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:0]");
  40. $this->assertEquals([], $result->data());
  41. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:1]");
  42. $this->assertEquals(["first"], $result->data());
  43. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:2]");
  44. $this->assertEquals(["first", "second"], $result->data());
  45. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
  46. $this->assertEquals(["first", "second"], $result->data());
  47. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[1:2]");
  48. $this->assertEquals(["second"], $result->data());
  49. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:1]");
  50. $this->assertEquals(["first", "second","third"], $result->data());
  51. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:3:0]");
  52. $this->assertEquals(["first", "second","third"], $result->data());
  53. }
  54. public function testFilterSlice_NegativeStartIndexes()
  55. {
  56. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:]");
  57. $this->assertEquals(["fourth", "fifth"], $result->data());
  58. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1:]");
  59. $this->assertEquals(["fifth"], $result->data());
  60. }
  61. /**
  62. * Negative end indexes
  63. * $[:-2]
  64. */
  65. public function testFilterSlice_NegativeEndIndexes()
  66. {
  67. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:-2]");
  68. $this->assertEquals(["first", "second", "third"], $result->data());
  69. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:-2]");
  70. $this->assertEquals(["first", "second", "third"], $result->data());
  71. }
  72. /**
  73. * Negative end indexes
  74. * $[:-2]
  75. */
  76. public function testFilterSlice_NegativeStartAndEndIndexes()
  77. {
  78. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:-1]");
  79. $this->assertEquals(["fourth"], $result->data());
  80. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-4:-2]");
  81. $this->assertEquals(["second", "third"], $result->data());
  82. }
  83. /**
  84. * Negative end indexes
  85. * $[:-2]
  86. */
  87. public function testFilterSlice_NegativeStartAndPositiveEnd()
  88. {
  89. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-2:2]");
  90. $this->assertEquals([], $result->data());
  91. }
  92. public function testFilterSlice_StepBy2()
  93. {
  94. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[0:4:2]");
  95. $this->assertEquals(["first", "third"], $result->data());
  96. }
  97. /**
  98. * The Last item
  99. * $[-1]
  100. */
  101. public function testFilterLastIndex()
  102. {
  103. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[-1]");
  104. $this->assertEquals(["fifth"], $result->data());
  105. }
  106. /**
  107. * Array index slice only end
  108. * $[:2]
  109. */
  110. public function testFilterSliceG()
  111. {
  112. // Fetch up to the second index
  113. $result = (new JSONPath(["first", "second", "third", "fourth", "fifth"]))->find("$[:2]");
  114. $this->assertEquals(["first", "second"], $result->data());
  115. }
  116. /**
  117. * $.store.books[(@.length-1)].title
  118. *
  119. * This notation is only partially implemented eg. hacked in
  120. */
  121. public function testChildQuery()
  122. {
  123. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[(@.length-1)].title");
  124. $this->assertEquals(['The Lord of the Rings'], $result->data());
  125. }
  126. /**
  127. * $.store.books[?(@.price < 10)].title
  128. * Filter books that have a price less than 10
  129. */
  130. public function testQueryMatchLessThan()
  131. {
  132. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[?(@.price < 10)].title");
  133. $this->assertEquals(['Sayings of the Century', 'Moby Dick'], $result->data());
  134. }
  135. /**
  136. * $..books[?(@.author == "J. R. R. Tolkien")]
  137. * Filter books that have a title equal to "..."
  138. */
  139. public function testQueryMatchEquals()
  140. {
  141. $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author == "J. R. R. Tolkien")].title');
  142. $this->assertEquals($results[0], 'The Lord of the Rings');
  143. }
  144. /**
  145. * $..books[?(@.author = 1)]
  146. * Filter books that have a title equal to "..."
  147. */
  148. public function testQueryMatchEqualsWithUnquotedInteger()
  149. {
  150. $results = (new JSONPath($this->exampleDataWithSimpleIntegers(rand(0, 1))))->find('$..features[?(@.value = 1)]');
  151. $this->assertEquals($results[0]->name, "foo");
  152. $this->assertEquals($results[1]->name, "baz");
  153. }
  154. /**
  155. * $..books[?(@.author != "J. R. R. Tolkien")]
  156. * Filter books that have a title not equal to "..."
  157. */
  158. public function testQueryMatchNotEqualsTo()
  159. {
  160. $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author != "J. R. R. Tolkien")].title');
  161. $this->assertcount(3, $results);
  162. $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
  163. $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author !== "J. R. R. Tolkien")].title');
  164. $this->assertcount(3, $results);
  165. $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
  166. $results = (new JSONPath($this->exampleData(rand(0, 1))))->find('$..books[?(@.author <> "J. R. R. Tolkien")].title');
  167. $this->assertcount(3, $results);
  168. $this->assertEquals(['Sayings of the Century', 'Sword of Honour', 'Moby Dick'], [$results[0], $results[1], $results[2]]);
  169. }
  170. /**
  171. * $.store.books[*].author
  172. */
  173. public function testWildcardAltNotation()
  174. {
  175. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.books[*].author");
  176. $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
  177. }
  178. /**
  179. * $..author
  180. */
  181. public function testRecursiveChildSearch()
  182. {
  183. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..author");
  184. $this->assertEquals(['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien'], $result->data());
  185. }
  186. /**
  187. * $.store.*
  188. * all things in store
  189. * the structure of the example data makes this test look weird
  190. */
  191. public function testWildCard()
  192. {
  193. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store.*");
  194. if (is_object($result[0][0])) {
  195. $this->assertEquals('Sayings of the Century', $result[0][0]->title);
  196. } else {
  197. $this->assertEquals('Sayings of the Century', $result[0][0]['title']);
  198. }
  199. if (is_object($result[1])) {
  200. $this->assertEquals('red', $result[1]->color);
  201. } else {
  202. $this->assertEquals('red', $result[1]['color']);
  203. }
  204. }
  205. /**
  206. * $.store..price
  207. * the price of everything in the store.
  208. */
  209. public function testRecursiveChildSearchAlt()
  210. {
  211. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$.store..price");
  212. $this->assertEquals([8.95, 12.99, 8.99, 22.99, 19.95], $result->data());
  213. }
  214. /**
  215. * $..books[2]
  216. * the third book
  217. */
  218. public function testRecursiveChildSearchWithChildIndex()
  219. {
  220. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[2].title");
  221. $this->assertEquals(["Moby Dick"], $result->data());
  222. }
  223. /**
  224. * $..books[(@.length-1)]
  225. */
  226. public function testRecursiveChildSearchWithChildQuery()
  227. {
  228. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[(@.length-1)].title");
  229. $this->assertEquals(["The Lord of the Rings"], $result->data());
  230. }
  231. /**
  232. * $..books[-1:]
  233. * Resturn the last results
  234. */
  235. public function testRecursiveChildSearchWithSliceFilter()
  236. {
  237. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[-1:].title");
  238. $this->assertEquals(["The Lord of the Rings"], $result->data());
  239. }
  240. /**
  241. * $..books[?(@.isbn)]
  242. * filter all books with isbn number
  243. */
  244. public function testRecursiveWithQueryMatch()
  245. {
  246. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..books[?(@.isbn)].isbn");
  247. $this->assertEquals(['0-553-21311-3', '0-395-19395-8'], $result->data());
  248. }
  249. /**
  250. * $..*
  251. * All members of JSON structure
  252. */
  253. public function testRecursiveWithWildcard()
  254. {
  255. $result = (new JSONPath($this->exampleData(rand(0, 1))))->find("$..*");
  256. $result = json_decode(json_encode($result), true);
  257. $this->assertEquals('Sayings of the Century', $result[0]['books'][0]['title']);
  258. $this->assertEquals(19.95, $result[26]);
  259. }
  260. /**
  261. * Tests direct key access.
  262. */
  263. public function testSimpleArrayAccess()
  264. {
  265. $result = (new JSONPath(['title' => 'test title']))->find('title');
  266. $this->assertEquals(['test title'], $result->data());
  267. }
  268. public function testFilteringOnNoneArrays()
  269. {
  270. $data = ['foo' => 'asdf'];
  271. $result = (new JSONPath($data))->find("$.foo.bar");
  272. $this->assertEquals([], $result->data());
  273. }
  274. public function testMagicMethods()
  275. {
  276. $fooClass = new JSONPathTestClass();
  277. $results = (new JSONPath($fooClass, JSONPath::ALLOW_MAGIC))->find('$.foo');
  278. $this->assertEquals(['bar'], $results->data());
  279. }
  280. public function testMatchWithComplexSquareBrackets()
  281. {
  282. $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'][?(@['@language']='en')]['@language']");
  283. $this->assertEquals(["en"], $result->data());
  284. }
  285. public function testQueryMatchWithRecursive()
  286. {
  287. $locations = $this->exampleDataLocations();
  288. $result = (new JSONPath($locations))->find("..[?(@.type == 'suburb')].name");
  289. $this->assertEquals(["Rosebank"], $result->data());
  290. }
  291. public function testFirst()
  292. {
  293. $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");
  294. $this->assertEquals(["@language" => "en"], $result->first()->data());
  295. }
  296. public function testLast()
  297. {
  298. $result = (new JSONPath($this->exampleDataExtra()))->find("$['http://www.w3.org/2000/01/rdf-schema#label'].*");
  299. $this->assertEquals(["@language" => "de"], $result->last()->data());
  300. }
  301. public function testSlashesInIndex()
  302. {
  303. $result = (new JSONPath($this->exampleDataWithSlashes()))->find("$['mediatypes']['image/png']");
  304. $this->assertEquals(
  305. [
  306. "/core/img/filetypes/image.png",
  307. ],
  308. $result->data()
  309. );
  310. }
  311. public function testCyrillicText()
  312. {
  313. $result = (new JSONPath(["трололо" => 1]))->find("$['трололо']");
  314. $this->assertEquals([1], $result->data());
  315. $result = (new JSONPath(["трололо" => 1]))->find("$.трололо");
  316. $this->assertEquals([1], $result->data());
  317. }
  318. public function testOffsetUnset()
  319. {
  320. $data = [
  321. "route" => [
  322. ["name" => "A", "type" => "type of A"],
  323. ["name" => "B", "type" => "type of B"],
  324. ],
  325. ];
  326. $data = json_encode($data);
  327. $jsonIterator = new JSONPath(json_decode($data));
  328. /** @var JSONPath $route */
  329. $route = $jsonIterator->offsetGet('route');
  330. $route->offsetUnset(0);
  331. $first = $route->first();
  332. $this->assertEquals("B", $first['name']);
  333. }
  334. public function testFirstKey()
  335. {
  336. // Array test for array
  337. $jsonPath = new JSONPath(['a' => 'A', 'b', 'B']);
  338. $firstKey = $jsonPath->firstKey();
  339. $this->assertEquals('a', $firstKey);
  340. // Array test for object
  341. $jsonPath = new JSONPath((object)['a' => 'A', 'b', 'B']);
  342. $firstKey = $jsonPath->firstKey();
  343. $this->assertEquals('a', $firstKey);
  344. }
  345. public function testLastKey()
  346. {
  347. // Array test for array
  348. $jsonPath = new JSONPath(['a' => 'A', 'b' => 'B', 'c' => 'C']);
  349. $lastKey = $jsonPath->lastKey();
  350. $this->assertEquals('c', $lastKey);
  351. // Array test for object
  352. $jsonPath = new JSONPath((object)['a' => 'A', 'b' => 'B', 'c' => 'C']);
  353. $lastKey = $jsonPath->lastKey();
  354. $this->assertEquals('c', $lastKey);
  355. }
  356. /**
  357. * Test: ensure trailing comma is stripped during parsing
  358. */
  359. public function testTrailingComma()
  360. {
  361. $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));
  362. $result = $jsonPath->find("$..book[0,1,2,]");
  363. $this->assertCount(3, $result);
  364. }
  365. /**
  366. * Test: ensure negative indexes return -n from last index
  367. */
  368. public function testNegativeIndex()
  369. {
  370. $jsonPath = new JSONPath(json_decode('{"store":{"book":[{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Evelyn Waugh","title":"Sword of Honour","price":12.99},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}],"bicycle":{"color":"red","price":19.95}},"expensive":10}'));
  371. $result = $jsonPath->find("$..book[-2]");
  372. $this->assertEquals("Herman Melville", $result[0]['author']);
  373. }
  374. public function testQueryAccessWithNumericalIndexes()
  375. {
  376. $jsonPath = new JSONPath(json_decode('{
  377. "result": {
  378. "list": [
  379. {
  380. "time": 1477526400,
  381. "o": "11.51000"
  382. },
  383. {
  384. "time": 1477612800,
  385. "o": "11.49870"
  386. }
  387. ]
  388. }
  389. }'));
  390. $result = $jsonPath->find("$.result.list[?(@.o == \"11.51000\")]");
  391. $this->assertEquals("11.51000", $result[0]->o);
  392. $jsonPath = new JSONPath(json_decode('{
  393. "result": {
  394. "list": [
  395. [
  396. 1477526400,
  397. "11.51000"
  398. ],
  399. [
  400. 1477612800,
  401. "11.49870"
  402. ]
  403. ]
  404. }
  405. }'));
  406. $result = $jsonPath->find("$.result.list[?(@[1] == \"11.51000\")]");
  407. $this->assertEquals("11.51000", $result[0][1]);
  408. }
  409. public function exampleData($asArray = true)
  410. {
  411. $json = '
  412. {
  413. "store":{
  414. "books":[
  415. {
  416. "category":"reference",
  417. "author":"Nigel Rees",
  418. "title":"Sayings of the Century",
  419. "price":8.95
  420. },
  421. {
  422. "category":"fiction",
  423. "author":"Evelyn Waugh",
  424. "title":"Sword of Honour",
  425. "price":12.99
  426. },
  427. {
  428. "category":"fiction",
  429. "author":"Herman Melville",
  430. "title":"Moby Dick",
  431. "isbn":"0-553-21311-3",
  432. "price":8.99
  433. },
  434. {
  435. "category":"fiction",
  436. "author":"J. R. R. Tolkien",
  437. "title":"The Lord of the Rings",
  438. "isbn":"0-395-19395-8",
  439. "price":22.99
  440. }
  441. ],
  442. "bicycle":{
  443. "color":"red",
  444. "price":19.95
  445. }
  446. }
  447. }';
  448. return json_decode($json, $asArray);
  449. }
  450. public function exampleDataExtra($asArray = true)
  451. {
  452. $json = '
  453. {
  454. "http://www.w3.org/2000/01/rdf-schema#label":[
  455. {
  456. "@language":"en"
  457. },
  458. {
  459. "@language":"de"
  460. }
  461. ]
  462. }
  463. ';
  464. return json_decode($json, $asArray);
  465. }
  466. public function exampleDataLocations($asArray = true)
  467. {
  468. $json = '
  469. {
  470. "name": "Gauteng",
  471. "type": "province",
  472. "child": {
  473. "name": "Johannesburg",
  474. "type": "city",
  475. "child": {
  476. "name": "Rosebank",
  477. "type": "suburb"
  478. }
  479. }
  480. }
  481. ';
  482. return json_decode($json, $asArray);
  483. }
  484. public function exampleDataWithSlashes($asArray = true)
  485. {
  486. $json = '
  487. {
  488. "features": [],
  489. "mediatypes": {
  490. "image/png": "/core/img/filetypes/image.png",
  491. "image/jpeg": "/core/img/filetypes/image.png",
  492. "image/gif": "/core/img/filetypes/image.png",
  493. "application/postscript": "/core/img/filetypes/image-vector.png"
  494. }
  495. }
  496. ';
  497. return json_decode($json, $asArray);
  498. }
  499. public function exampleDataWithSimpleIntegers($asArray = true)
  500. {
  501. $json = '
  502. {
  503. "features": [{"name": "foo", "value": 1},{"name": "bar", "value": 2},{"name": "baz", "value": 1}]
  504. }
  505. ';
  506. return json_decode($json, $asArray);
  507. }
  508. }
  509. class JSONPathTestClass
  510. {
  511. protected $attributes = [
  512. 'foo' => 'bar',
  513. ];
  514. public function __get($key)
  515. {
  516. return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
  517. }
  518. }