Skip to content

Commit

Permalink
Merge pull request #59 from drugan/master
Browse files Browse the repository at this point in the history
Allow filter keys with a regex match
  • Loading branch information
Galbar authored Aug 18, 2022
2 parents 254853c + 7db0931 commit 2cb34e8
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 6 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ This is a [JSONPath](http://goessner.net/articles/JsonPath/) implementation for
This implementation features all elements in the specification except the `()` operator (in the spcecification there is the `$..a[(@.length-1)]`, but this can be achieved with `$..a[-1]` and the latter is simpler).

On top of this it implements some extended features:
* Regex match comparisons (p.e. `$.store.book[?(@.author =~ /.*Tolkien/)]`)
* Regex match key (p.e. `$.*[?(/^bo{2}k$/)]` or `$[?(/a\wthors/)]`).
* Regex match value comparisons (p.e. `$.store.book[?(@.author =~ /.*Tolkien/)]`)
* For the child operator `[]` there is no need to surround child names with quotes (p.e. `$.[store][book, bicycle]`) except if the name of the field is a non-valid javascript variable name.
* `.length` can be used to get the length of a string, get the length of an array and to check if a node has children.

Expand Down Expand Up @@ -100,7 +101,7 @@ var_name = [\w\_\$^\d][\w\-\$]*
number = ([0-9]+(\.[0-9]*) | ([0-9]*\.[0-9]+))
string = ('\''.*?'\'' | '"'.*?'"')
boolean = ('true' | 'false')
regpattern = '/'.*?'/'
regpattern = '/'.*?'/i?x?'
null = 'null'
index = -?[0-9]+
Expand All @@ -116,7 +117,7 @@ childfilter = '[' ('*' | namelist | indexlist | arrayslice | filterexpr) ']'
namelist = var_name (',' (var_name | '\'' .*? '\'' | '"' .*? '"'))*
indexlist = index (',' index)*
arrayslice = index? ':' index? ':' index?
filterexpr = '?(' ors ')'
filterexpr = '?(' ors ' | regpattern)'
ors = ands (' ' ( 'or' | '\|\|' ) ' ' ands)*
ands = expr (' ' ( 'and' | '&&' ) ' ' expr)*
Expand Down Expand Up @@ -211,6 +212,7 @@ JsonPath | Result
`$[store]` | The store.
`$['store']` | The store.
`$..book[*][title, 'category', "author"]` | title, category and author of all books.
See more examples in the `./tests/Galbar/JsonPath` folder.

Test
====
Expand Down
2 changes: 1 addition & 1 deletion src/Galbar/JsonPath/Language/Regex.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Regex

// Conditional expressions
const EXPR_STRING = '/^(?:\'(.*)\'|"(.*)")$/';
const EXPR_REGEX = '/^\/.*\/$/';
const EXPR_REGEX = '/^\/.*\/i?x?$/';
const BINOP_COMP = '/^(.+)\s*(==|!=|<=|>=|<|>|=\~)\s*(.+)$/';
const BINOP_OR = '/\s+(or|\|\|)\s+/';
const BINOP_AND = '/\s+(and|&&)\s+/';
Expand Down
9 changes: 7 additions & 2 deletions src/Galbar/JsonPath/Operation/SelectChildren.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ function($x) { return intval(trim($x)); },
) {
$hasDiverged = true;
$subexpr = substr($contents, 2, $contentsLen - 3);
foreach ($partial as &$child) {
if (Expression\BooleanExpression::evaluate($root, $child, $subexpr)) {
$is_regex = preg_match(Language\Regex::EXPR_REGEX, $subexpr);
foreach ($partial as $key => &$child) {
if ($is_regex) {
if (preg_match($subexpr, $key)) {
$result[] = &$child;
}
} else if (Expression\BooleanExpression::evaluate($root, $child, $subexpr)) {
$result[] = &$child;
}
}
Expand Down
12 changes: 12 additions & 0 deletions tests/Galbar/JsonPath/JsonObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,18 @@ public function testGetProvider()
),
"$..*[?(@.author =~ /.*Tolkien/)].title"
),
array(
array(
"The Lord of the Rings"
),
"$..*[?(@.author =~ /.*tolkien/i)].title"
),
array(
array(
"The Lord of the Rings"
),
"$..*[?(@.author =~ / J.\ R.\ R.\ Tolkien /x)].title"
),
array(
array(
"red"
Expand Down
319 changes: 319 additions & 0 deletions tests/Galbar/JsonPath/JsonPathKeyRegexMatchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
<?php
/**
* Copyright 2019 Vlad Proshin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Tests;

use JsonPath\JsonObject;
use JsonPath\InvalidJsonException;

/**
* Class JsonPathKeyRegexMatchTest
* @author Vlad Proshin
*/
class JsonPathKeyRegexMatchTest extends \PHPUnit_Framework_TestCase
{
private $json = '
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95,
"available": true
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99,
"available": false
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99,
"available": true
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99,
"available": false
}
],
"bicycle": {
"color": "red",
"price": 19.95,
"available": true
},
"motorcycle": {
"color": "blue",
"price": 999.99,
"available": false
}
},
"authors": [
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien"
],
"author-link": {
"Nigel Rees": {
"href": "https://en.wikipedia.org/wiki/Nigel_Rees",
"placed-at": "wikipedia"
},
"Evelyn Waugh": {
"href": "https://www.britannica.com/biography/Evelyn-Waugh",
"placed-at": "britannica"
},
"Herman Melville": {
"href": "https://www.britannica.com/biography/Herman-Melville",
"placed-at": "britannica"
},
"J. R. R. Tolkien": {
"href": "https://www.tolkiensociety.org/author/biography/",
"placed-at": "tolkiensociety"
}
},
"author-biography": [
{
"Nigel Rees": [
{
"born": "5 June 1944",
"age": 78
}
],
"Evelyn Waugh": [
{
"born": "28 October 1903",
"died": "10 April 1966",
"resting-place": "Combe Florey"
}
],
"Herman Melville": [
{
"born": "August 1, 1819",
"died": "September 28, 1891",
"resting-place": "Woodlawn"
}
],
"J. R. R. Tolkien": [
{
"born": "3 January 1892",
"died": "2 September 1973",
"resting-place": "Bournemouth"
}
]
}
]
}
';

/**
* @throws InvalidJsonException
*/
public function testRootObjectKeyMatch()
{
$jsonPath = '$[?(/^sto?re+$/)].bicycle.price';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
[19.95],
$result
);

$jsonPath = '$[?(/\-link$/)].*.placed-at';
$result = $jsonObject->get($jsonPath);
$this->assertEquals([
"wikipedia",
"britannica",
"britannica",
"tolkiensociety"
],
$result
);
}

/**
* @throws InvalidJsonException
*/
public function testRootArrayKeyMatch()
{
$jsonPath = '$[?(/auth.*s$/)]';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals([
[
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien"
]
],
$result
);

$jsonPath = '$[?(/^\w*\-biography$/)]..*.age';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
[78],
$result
);
}

/**
* @throws InvalidJsonException
*/
public function testNestedObjectKeyMatch()
{
$jsonPath = '$..*[?(/^\w*[^-]cycle$/)].color';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["red", "blue"],
$result
);

$jsonPath = '$.*[?(/^[A-Z]erman\sMelvil{2}e$/)].href';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["https://www.britannica.com/biography/Herman-Melville"],
$result
);

$jsonPath = '$.*[?(/^[A-Z]erman\sMelvil{2}e$/)][0]';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
false,
$result
);
}


/**
* @throws InvalidJsonException
*/
public function testNestedArrayKeyMatch()
{
$jsonPath = '$.*[?(/^bo{2}k$/)][*].isbn';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["0-553-21311-3", "0-395-19395-8"],
$result
);

$jsonPath = '$.*[?(/R\.?/)][0]';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
false,
$result
);

$jsonPath = '$..*[?(/R\.?/)][0].born';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["5 June 1944", "3 January 1892"],
$result
);

$jsonPath = '$..*[?(/^[A-Z]\w*\s+[A-Z]\w*$/)][*].resting-place';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["Combe Florey", "Woodlawn"],
$result
);
}

/**
* @throws InvalidJsonException
*/
public function testRecursiveKeyMatch()
{
$jsonPath = '$..*[?(/^author|age|died$/)]';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals([
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien",
78,
"10 April 1966",
"September 28, 1891",
"2 September 1973"
],
$result
);
}

/**
* @throws InvalidJsonException
*/
public function testPcreCaseInsensitiveModifier()
{
$jsonPath = '$.*[?(/^evelyn.waugh$/i)].href';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["https://www.britannica.com/biography/Evelyn-Waugh"],
$result
);

$jsonPath = '$.*[?(/^evelyn.waugh$/)].href';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
false,
$result
);
}

/**
* @throws InvalidJsonException
*/
public function testPcreIgnoreWhiteSpaceModifier()
{
$jsonPath = '$.*[?(/^J. R. R. Tolkien$/)].href';
$jsonObject = new JsonObject($this->json);
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["https://www.tolkiensociety.org/author/biography/"],
$result
);

$jsonPath = '$.*[?(/^J. R. R. Tolkien$/x)].href';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
false,
$result
);

$jsonPath = '$.*[?(/^ J.\s R.\s R.\s Tolkien $/x)].href';
$result = $jsonObject->get($jsonPath);
$this->assertEquals(
["https://www.tolkiensociety.org/author/biography/"],
$result
);
}
}

0 comments on commit 2cb34e8

Please sign in to comment.