Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jsonpath_rust_bindings"
version = "1.1.0"
version = "1.1.1"
edition = "2021"

[lib]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ queries = [
'$..book[2:]',
'$.store.book[?(@.price<10)]',
'$..book[?(@.price<=$.expensive)]',
"$..book[?(@.author ~= '.*Rees')].price",
'$..book[?match(@.author, "(?i).*rees.*")].price',
'$..*',
]

Expand Down
57 changes: 16 additions & 41 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,73 +61,53 @@ impl Finder {

// Execute JSONPath query, return list of results containing data and paths
fn find(self_: PyRef<'_, Self>, query: String) -> PyResult<Vec<JsonPathResult>> {
find_internal_path_value(&self_.value, &query, |_| true)
find_path_value_internal(&self_.value, &query)
}

// Execute JSONPath query, return only found data values
fn find_data(self_: PyRef<'_, Self>, query: String) -> PyResult<Vec<Py<PyAny>>> {
find_internal_data(&self_.value, &query, |_| true)
find_data_internal(&self_.value, &query)
}

// Execute JSONPath query, return only found absolute paths
fn find_absolute_path(self_: PyRef<'_, Self>, query: String) -> PyResult<Vec<String>> {
find_internal_path(&self_.value, &query, |_| true)
find_paths_internal(&self_.value, &query)
}
}

// Execute JSONPath query and return processed results
fn execute_query<'a>(
value: &'a Value,
query: &str,
predicate: impl Fn(&QueryRef<Value>) -> bool,
) -> PyResult<Vec<QueryRef<'a, Value>>> {
fn execute_query<'a>(value: &'a Value, query: &str) -> PyResult<Vec<QueryRef<'a, Value>>> {
let parsed_query = parse_query(query)?;
let processed = js_path_process(&parsed_query, value)
.map_err(|err| PyValueError::new_err(err.to_string()))?;

Ok(processed.into_iter().filter(predicate).collect())
Ok(processed.into_iter().collect())
}

// Execute query and return JsonPathResult list
fn find_internal_path_value(
value: &Value,
query: &str,
predicate: impl Fn(&QueryRef<Value>) -> bool,
) -> PyResult<Vec<JsonPathResult>> {
let filtered = execute_query(value, query, predicate)?;
fn find_path_value_internal(value: &Value, query: &str) -> PyResult<Vec<JsonPathResult>> {
let result = execute_query(value, query)?;

Python::attach(|py| {
filtered
result
.into_iter()
.map(|v| map_json_path_value(py, v))
.collect()
})
}

// Execute query and return data value list
fn find_internal_data(
value: &Value,
query: &str,
predicate: impl Fn(&QueryRef<Value>) -> bool,
) -> PyResult<Vec<Py<PyAny>>> {
let filtered = execute_query(value, query, predicate)?;

Python::attach(|py| {
filtered
.into_iter()
.map(|v| map_json_value(py, v))
.collect()
})
fn find_data_internal(value: &Value, query: &str) -> PyResult<Vec<Py<PyAny>>> {
let result = execute_query(value, query)?;
Python::attach(|py| result.into_iter().map(|v| map_json_value(py, v)).collect())
}

// Execute query and return path string list
fn find_internal_path(
value: &Value,
query: &str,
predicate: impl Fn(&QueryRef<Value>) -> bool,
) -> PyResult<Vec<String>> {
let filtered = execute_query(value, query, predicate)?;
filtered.into_iter().map(|v| map_json_path(v)).collect()
fn find_paths_internal(value: &Value, query: &str) -> PyResult<Vec<String>> {
Ok(execute_query(value, query)?
.into_iter()
.map(|v| v.path())
.collect::<Vec<_>>())
}

// Map QueryRef<Value> to JsonPathResult
Expand All @@ -141,11 +121,6 @@ fn map_json_path_value(py: Python, jpv: QueryRef<Value>) -> PyResult<JsonPathRes
})
}

// Extract path string from QueryRef<Value>
fn map_json_path(jpv: QueryRef<Value>) -> PyResult<String> {
Ok(jpv.path())
}

// Convert value in QueryRef<Value> to Python object
fn map_json_value(py: Python, jpv: QueryRef<Value>) -> PyResult<Py<PyAny>> {
let val = jpv.val();
Expand Down
192 changes: 63 additions & 129 deletions tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,137 +64,71 @@ def test_repr(sample_data):
)


def test_all_queries(sample_data):
queries = [
"$.store.book[*].author",
"$..book[?(@.isbn)]",
"$.store.*",
"$..author",
"$.store..price",
"$..book[2]",
"$..book[-2]",
"$..book[0,1]",
"$..book[:2]",
"$..book[1:2]",
"$..book[-2:]",
"$..book[2:]",
"$.store.book[?(@.price<10)]",
"$..book[?(@.price<=$.expensive)]",
# "$..book[?@.author ~= '(?i)REES']",
"$..*",
]
queries_results = [
"""[JsonPathResult(data='Nigel Rees', path="$['store']['book'][0]['author']"),
JsonPathResult(data='Evelyn Waugh', path="$['store']['book'][1]['author']"),
JsonPathResult(data='Herman Melville', path="$['store']['book'][2]['author']"),
JsonPathResult(data='J. R. R. Tolkien', path="$['store']['book'][3]['author']")]""",
"""[JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]"),
JsonPathResult(data={'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'},
path="$['store']['book'][3]")]""",
"""[JsonPathResult(data={'color': 'red', 'price': 19.95}, path="$['store']['bicycle']"),
JsonPathResult(data=[{'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
{'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'},
{'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
{'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'}],
path="$['store']['book']")]""",
"""[JsonPathResult(data='Nigel Rees', path="$['store']['book'][0]['author']"),
JsonPathResult(data='Evelyn Waugh', path="$['store']['book'][1]['author']"),
JsonPathResult(data='Herman Melville', path="$['store']['book'][2]['author']"),
JsonPathResult(data='J. R. R. Tolkien', path="$['store']['book'][3]['author']")]""",
"""[JsonPathResult(data=19.95, path="$['store']['bicycle']['price']"),
JsonPathResult(data=8.95, path="$['store']['book'][0]['price']"),
JsonPathResult(data=12.99, path="$['store']['book'][1]['price']"),
JsonPathResult(data=8.99, path="$['store']['book'][2]['price']"),
JsonPathResult(data=22.99, path="$['store']['book'][3]['price']")]""",
"""[JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]")]""",
"""[JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]")]""",
"""[JsonPathResult(data={'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
path="$['store']['book'][0]"),
JsonPathResult(data={'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'}, path="$['store']['book'][1]")]""",
"""[JsonPathResult(data={'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
path="$['store']['book'][0]"),
JsonPathResult(data={'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'}, path="$['store']['book'][1]")]""",
"""[JsonPathResult(data={'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'},
path="$['store']['book'][1]")]""",
"""[JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]"),
JsonPathResult(data={'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'},
path="$['store']['book'][3]")]""",
"""[JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]"),
JsonPathResult(data={'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'},
path="$['store']['book'][3]")]""",
"""[JsonPathResult(data={'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
path="$['store']['book'][0]"),
JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]")]""",
"""[JsonPathResult(data={'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
path="$['store']['book'][0]"),
JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]")]""",
"""[JsonPathResult(data=10, path="$['expensive']"),
JsonPathResult(data={'bicycle': {'color': 'red', 'price': 19.95},
'book': [{'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
{'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'},
{'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
{'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'}]},
path="$['store']"),
JsonPathResult(data={'color': 'red', 'price': 19.95}, path="$['store']['bicycle']"),
JsonPathResult(data=[{'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
{'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'},
{'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
{'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'}],
path="$['store']['book']"),
JsonPathResult(data='red', path="$['store']['bicycle']['color']"),
JsonPathResult(data=19.95, path="$['store']['bicycle']['price']"),
JsonPathResult(data={'author': 'Nigel Rees', 'category': 'reference', 'price': 8.95, 'title': 'Sayings of the Century'},
path="$['store']['book'][0]"),
JsonPathResult(data={'author': 'Evelyn Waugh', 'category': 'fiction', 'price': 12.99, 'title': 'Sword of Honour'}, path="$['store']['book'][1]"),
JsonPathResult(data={'author': 'Herman Melville', 'category': 'fiction', 'isbn': '0-553-21311-3', 'price': 8.99, 'title': 'Moby Dick'},
path="$['store']['book'][2]"),
JsonPathResult(data={'author': 'J. R. R. Tolkien', 'category': 'fiction', 'isbn': '0-395-19395-8', 'price': 22.99, 'title': 'The Lord of the Rings'},
path="$['store']['book'][3]"),
JsonPathResult(data='Nigel Rees', path="$['store']['book'][0]['author']"),
JsonPathResult(data='reference', path="$['store']['book'][0]['category']"),
JsonPathResult(data=8.95, path="$['store']['book'][0]['price']"),
JsonPathResult(data='Sayings of the Century', path="$['store']['book'][0]['title']"),
JsonPathResult(data='Evelyn Waugh', path="$['store']['book'][1]['author']"),
JsonPathResult(data='fiction', path="$['store']['book'][1]['category']"),
JsonPathResult(data=12.99, path="$['store']['book'][1]['price']"),
JsonPathResult(data='Sword of Honour', path="$['store']['book'][1]['title']"),
JsonPathResult(data='Herman Melville', path="$['store']['book'][2]['author']"),
JsonPathResult(data='fiction', path="$['store']['book'][2]['category']"),
JsonPathResult(data='0-553-21311-3', path="$['store']['book'][2]['isbn']"),
JsonPathResult(data=8.99, path="$['store']['book'][2]['price']"),
JsonPathResult(data='Moby Dick', path="$['store']['book'][2]['title']"),
JsonPathResult(data='J. R. R. Tolkien', path="$['store']['book'][3]['author']"),
JsonPathResult(data='fiction', path="$['store']['book'][3]['category']"),
JsonPathResult(data='0-395-19395-8', path="$['store']['book'][3]['isbn']"),
JsonPathResult(data=22.99, path="$['store']['book'][3]['price']"),
JsonPathResult(data='The Lord of the Rings', path="$['store']['book'][3]['title']")]""",
]
f = Finder(sample_data)
res = []
res_data = []
res_absolute_path = []
for query, queries_result in zip(queries, queries_results):
temp_res = f.find(query)
res.extend(temp_res)
res_data.extend(f.find_data(query))
res_absolute_path.extend(f.find_absolute_path(query))
assert str(temp_res) == queries_result.replace("\n", "")
assert [r.data for r in res] == res_data
assert [r.path for r in res] == res_absolute_path
# print(query)
# print(f.find(query), '\n')
# print('----------')


def test_overflow():
big_number = 18446744005107584948
f = Finder({"test": big_number})
res = f.find('$.test')[0].data
assert res == big_number


QUERIES = [
"$.store.book[*].author",
"$..book[?(@.isbn)]",
"$.store.*",
"$..author",
"$.store..price",
"$..book[2]",
"$..book[-2]",
"$..book[0,1]",
"$..book[:2]",
"$..book[1:2]",
"$..book[-2:]",
"$..book[2:]",
"$.store.book[?(@.price<10)]",
"$..book[?(@.price<=$.expensive)]",
"$..*",
r'$..book[?match(@.author, "(?i).*rees.*")]',
]

@pytest.mark.parametrize("query", QUERIES, ids=QUERIES)
def test_smoke(sample_data, query):
f = Finder(sample_data)

results = f.find(query)
assert isinstance(results, list)
assert all(isinstance(r, JsonPathResult) for r in results)

assert [r.data for r in results] == f.find_data(query)
assert [r.path for r in results] == f.find_absolute_path(query)


def test_authors_data_and_paths(sample_data):
f = Finder(sample_data)
results = f.find("$.store.book[*].author")
data, paths = _extract(results)

assert data == [
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville",
"J. R. R. Tolkien",
]
assert paths == [
"$['store']['book'][0]['author']",
"$['store']['book'][1]['author']",
"$['store']['book'][2]['author']",
"$['store']['book'][3]['author']",
]

def test_bicycle_color_value_and_path(sample_data):
f = Finder(sample_data)
results = f.find("$.store.bicycle.color")
data, paths = _extract(results)

assert data == ["red"]
assert paths == ["$['store']['bicycle']['color']"]


def _extract(results):
assert all(isinstance(r, JsonPathResult) for r in results)
return [r.data for r in results], [r.path for r in results]
Loading