simple_jsonpath 0.1.2__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,9 +7,6 @@ name: CI
7
7
 
8
8
  on:
9
9
  push:
10
- branches:
11
- - main
12
- - master
13
10
  tags:
14
11
  - '*'
15
12
  pull_request:
@@ -40,7 +37,7 @@ jobs:
40
37
  - uses: actions/checkout@v6
41
38
  - uses: actions/setup-python@v6
42
39
  with:
43
- python-version: 3.x
40
+ python-version: '>=3.10 <3.15'
44
41
  - name: Build wheels
45
42
  uses: PyO3/maturin-action@v1
46
43
  with:
@@ -71,7 +68,7 @@ jobs:
71
68
  - uses: actions/checkout@v6
72
69
  - uses: actions/setup-python@v6
73
70
  with:
74
- python-version: 3.x
71
+ python-version: '>=3.10 <3.15'
75
72
  - name: Build wheels
76
73
  uses: PyO3/maturin-action@v1
77
74
  with:
@@ -103,7 +100,7 @@ jobs:
103
100
  - uses: actions/checkout@v6
104
101
  - uses: actions/setup-python@v6
105
102
  with:
106
- python-version: 3.13
103
+ python-version: '>=3.10 <3.15'
107
104
  architecture: ${{ matrix.platform.python_arch }}
108
105
  - name: Build wheels
109
106
  uses: PyO3/maturin-action@v1
@@ -130,7 +127,7 @@ jobs:
130
127
  - uses: actions/checkout@v6
131
128
  - uses: actions/setup-python@v6
132
129
  with:
133
- python-version: 3.x
130
+ python-version: '>=3.10 <3.15'
134
131
  - name: Build wheels
135
132
  uses: PyO3/maturin-action@v1
136
133
  with:
@@ -70,3 +70,6 @@ docs/_build/
70
70
 
71
71
  # Pyenv
72
72
  .python-version
73
+
74
+ path.py
75
+ device.json
@@ -231,17 +231,6 @@ dependencies = [
231
231
  "syn",
232
232
  ]
233
233
 
234
- [[package]]
235
- name = "pythonize"
236
- version = "0.28.0"
237
- source = "registry+https://github.com/rust-lang/crates.io-index"
238
- checksum = "0b79f670c9626c8b651c0581011b57b6ba6970bb69faf01a7c4c0cfc81c43f95"
239
- dependencies = [
240
- "pyo3",
241
- "serde",
242
- "serde_json",
243
- ]
244
-
245
234
  [[package]]
246
235
  name = "quote"
247
236
  version = "1.0.45"
@@ -427,13 +416,14 @@ dependencies = [
427
416
 
428
417
  [[package]]
429
418
  name = "simple_jsonpath"
430
- version = "0.1.2"
419
+ version = "0.2.2"
431
420
  dependencies = [
432
421
  "pyo3",
433
- "pythonize",
434
422
  "rstest",
423
+ "serde",
435
424
  "serde_json",
436
425
  "serde_json_path",
426
+ "serde_json_path_core",
437
427
  ]
438
428
 
439
429
  [[package]]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "simple_jsonpath"
3
- version = "0.1.2"
3
+ version = "0.2.2"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -12,9 +12,14 @@ crate-type = ["cdylib"]
12
12
  [dependencies]
13
13
  pyo3 = { version = "0.28.2", features = ["serde"] }
14
14
  serde_json_path = { git = "https://github.com/seojumper/serde_json_path.git", branch = "quotes" }
15
+ serde_json_path_core = { git = "https://github.com/seojumper/serde_json_path.git", branch = "quotes" }
15
16
  serde_json = "1.0.149"
16
- pythonize = { version = "0.28.0", features = ["serde_json"] }
17
+ serde = { version = "1.0.228", features = ["derive"] }
17
18
 
18
19
 
19
20
  [dev-dependencies]
20
21
  rstest = "0.26.1"
22
+
23
+ [profile.release]
24
+ lto = true
25
+ codegen-units = 1
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2026 Jeremy Spencer
2
+
3
+ Permission is hereby granted, free of charge, to any
4
+ person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the
6
+ Software without restriction, including without
7
+ limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software
10
+ is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice
14
+ shall be included in all copies or substantial portions
15
+ of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_jsonpath
3
+ Version: 0.2.2
4
+ Classifier: Intended Audience :: Developers
5
+ Classifier: Programming Language :: Rust
6
+ Classifier: Programming Language :: Python :: Implementation :: CPython
7
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ License-File: LICENSE
15
+ Summary: A simple - yet quick - JSONPath implementation for querying JSON data.
16
+ Keywords: jsonpath
17
+ Author-email: Jeremy Spencer <seojumper@gmail.com>
18
+ License-Expression: MIT
19
+ Requires-Python: >=3.10, <3.15
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+ Project-URL: Homepage, https://github.com/seojumper/simple_jsonpath
22
+
23
+ # simple_jsonpath
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install simple_jsonpath
29
+ ```
30
+
31
+ ## About
32
+
33
+ This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
34
+
35
+ ## Use
36
+
37
+ This module exposes a single simple type - a ***JsonPath*** which has two methods after instantiation.
38
+
39
+ - ***set_data()***: sets the data that will be queried against. If multiple queries will be performed against a single piece of JSON data, this helps with the type conversion cost involved. This function can be called with providing new data whenever the inner data held is wished to be changed while retaining already complied paths (useful for querying multiple similarly structured documents).
40
+ - ***find()***: given a path that is wished to be found in the previously set data, this function will perform the query logic. Mulitple calls to **find()** will query against the previously 'set' data.
41
+ - ***find_located()***: given a path that is wished to be found in the previously set data, this function can return a list of ***LocatedNode*** objects. Each ***LocatedNode*** object will have attributes related to the path where the node was located as well as their corresponding data. This method is slower than ***find()***, so should ideally only be used when path information for the found nodes is needed.
42
+
43
+ ## Examples
44
+
45
+ ### 'Find' Example
46
+
47
+ ```python
48
+ from simple_jsonpath import JsonPath
49
+
50
+
51
+ json_data = {
52
+ "address": {
53
+ "prefix-list": [
54
+ {
55
+ "prefix": "2001:db8::1/64",
56
+ "eui-64": [
57
+ None
58
+ ]
59
+ }
60
+ ],
61
+ "link-local-address": [
62
+ {
63
+ "address": "fe80::1",
64
+ "link-local": [
65
+ None
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ }
71
+
72
+ # Instantiates the primary class
73
+ finder = JsonPath()
74
+
75
+ # Sets the data that is desired to be queried against
76
+ finder.set_data(json_data)
77
+
78
+ # A path is provided to query against the 'set' data. The path is internally parsed > used to qeury against the 'set' dataset.
79
+ # Notice that this implementaion allows for escaping of specials characters shorthand path syntax with single or double quotes
80
+ results = finder.find("$.address.'prefix-list'[*].prefix")
81
+
82
+ for data in results:
83
+ # Access the found node.
84
+ print(f"{data}")
85
+ # 2001:db8::1/64
86
+
87
+ ```
88
+
89
+ The inner implementation stores previously parsed 'paths'. This allows repeatedly used paths to bypass the parsing step invovled.
90
+
91
+ This is ideal for situations where multiple similar JSON documents will be searched in succession.
92
+
93
+ The same ***JsonPath*** object can then be reused with new data sets by calling ***set_data()*** on it again, and any previously parsed paths by the object will be retained.
94
+
95
+ Only when moving onto data of differing structure would it be potentially advisable to instantiate a new ***JsonPath*** object.
96
+
97
+ ### 'Find Located' Example
98
+
99
+ ```python
100
+ from simple_jsonpath import JsonPath, LocatedNode
101
+
102
+
103
+ json_data = {
104
+ "items": [
105
+ {
106
+ "address": {
107
+ "prefix-list": [
108
+ {
109
+ "prefix": "2001:db8::1/64",
110
+ "eui-64": [
111
+ None
112
+ ]
113
+ }
114
+ ],
115
+ "link-local-address": [
116
+ {
117
+ "address": "fe80::1",
118
+ "link-local": [
119
+ None
120
+ ]
121
+ }
122
+ ]
123
+ }
124
+ },
125
+ {
126
+ "address": {
127
+ "prefix-list": [
128
+ {
129
+ "prefix": "2001:db8::1/64",
130
+ "eui-64": [
131
+ None
132
+ ]
133
+ }
134
+ ],
135
+ "link-local-address": [
136
+ {
137
+ "address": "fe80::1",
138
+ "link-local": [
139
+ None
140
+ ]
141
+ }
142
+ ]
143
+ }
144
+ }
145
+ ]
146
+ }
147
+
148
+ # Instantiates the primary class
149
+ finder = JsonPath()
150
+
151
+ # Sets the data that is desired to be queried against
152
+ finder.set_data(json_data)
153
+
154
+ # Now we are interested in the path information where matches were found as well as the data
155
+ results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-list'[*].prefix")
156
+
157
+ # Iterate through each found LocatedNode object
158
+ for data in results:
159
+
160
+ # Print the normalized full path where the node was found
161
+ print(f"{data.full_path}")
162
+ # $['items'][0]['address']['prefix-list'][0]['prefix']
163
+
164
+ # Iterate over the components of the found path
165
+ # Returned elements will either be a 'str' for keys or 'int' for index values
166
+ print(f"{', '.join([str(component) for component in data.path_components])}")
167
+ # $, items, 0, adddress, prefix-list, 0, prefix
168
+
169
+ # Access the found node.
170
+ print(f"{data.node}")
171
+ # 2001:db8::1/64
172
+ ```
173
+
@@ -0,0 +1,150 @@
1
+ # simple_jsonpath
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pip install simple_jsonpath
7
+ ```
8
+
9
+ ## About
10
+
11
+ This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
12
+
13
+ ## Use
14
+
15
+ This module exposes a single simple type - a ***JsonPath*** which has two methods after instantiation.
16
+
17
+ - ***set_data()***: sets the data that will be queried against. If multiple queries will be performed against a single piece of JSON data, this helps with the type conversion cost involved. This function can be called with providing new data whenever the inner data held is wished to be changed while retaining already complied paths (useful for querying multiple similarly structured documents).
18
+ - ***find()***: given a path that is wished to be found in the previously set data, this function will perform the query logic. Mulitple calls to **find()** will query against the previously 'set' data.
19
+ - ***find_located()***: given a path that is wished to be found in the previously set data, this function can return a list of ***LocatedNode*** objects. Each ***LocatedNode*** object will have attributes related to the path where the node was located as well as their corresponding data. This method is slower than ***find()***, so should ideally only be used when path information for the found nodes is needed.
20
+
21
+ ## Examples
22
+
23
+ ### 'Find' Example
24
+
25
+ ```python
26
+ from simple_jsonpath import JsonPath
27
+
28
+
29
+ json_data = {
30
+ "address": {
31
+ "prefix-list": [
32
+ {
33
+ "prefix": "2001:db8::1/64",
34
+ "eui-64": [
35
+ None
36
+ ]
37
+ }
38
+ ],
39
+ "link-local-address": [
40
+ {
41
+ "address": "fe80::1",
42
+ "link-local": [
43
+ None
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ }
49
+
50
+ # Instantiates the primary class
51
+ finder = JsonPath()
52
+
53
+ # Sets the data that is desired to be queried against
54
+ finder.set_data(json_data)
55
+
56
+ # A path is provided to query against the 'set' data. The path is internally parsed > used to qeury against the 'set' dataset.
57
+ # Notice that this implementaion allows for escaping of specials characters shorthand path syntax with single or double quotes
58
+ results = finder.find("$.address.'prefix-list'[*].prefix")
59
+
60
+ for data in results:
61
+ # Access the found node.
62
+ print(f"{data}")
63
+ # 2001:db8::1/64
64
+
65
+ ```
66
+
67
+ The inner implementation stores previously parsed 'paths'. This allows repeatedly used paths to bypass the parsing step invovled.
68
+
69
+ This is ideal for situations where multiple similar JSON documents will be searched in succession.
70
+
71
+ The same ***JsonPath*** object can then be reused with new data sets by calling ***set_data()*** on it again, and any previously parsed paths by the object will be retained.
72
+
73
+ Only when moving onto data of differing structure would it be potentially advisable to instantiate a new ***JsonPath*** object.
74
+
75
+ ### 'Find Located' Example
76
+
77
+ ```python
78
+ from simple_jsonpath import JsonPath, LocatedNode
79
+
80
+
81
+ json_data = {
82
+ "items": [
83
+ {
84
+ "address": {
85
+ "prefix-list": [
86
+ {
87
+ "prefix": "2001:db8::1/64",
88
+ "eui-64": [
89
+ None
90
+ ]
91
+ }
92
+ ],
93
+ "link-local-address": [
94
+ {
95
+ "address": "fe80::1",
96
+ "link-local": [
97
+ None
98
+ ]
99
+ }
100
+ ]
101
+ }
102
+ },
103
+ {
104
+ "address": {
105
+ "prefix-list": [
106
+ {
107
+ "prefix": "2001:db8::1/64",
108
+ "eui-64": [
109
+ None
110
+ ]
111
+ }
112
+ ],
113
+ "link-local-address": [
114
+ {
115
+ "address": "fe80::1",
116
+ "link-local": [
117
+ None
118
+ ]
119
+ }
120
+ ]
121
+ }
122
+ }
123
+ ]
124
+ }
125
+
126
+ # Instantiates the primary class
127
+ finder = JsonPath()
128
+
129
+ # Sets the data that is desired to be queried against
130
+ finder.set_data(json_data)
131
+
132
+ # Now we are interested in the path information where matches were found as well as the data
133
+ results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-list'[*].prefix")
134
+
135
+ # Iterate through each found LocatedNode object
136
+ for data in results:
137
+
138
+ # Print the normalized full path where the node was found
139
+ print(f"{data.full_path}")
140
+ # $['items'][0]['address']['prefix-list'][0]['prefix']
141
+
142
+ # Iterate over the components of the found path
143
+ # Returned elements will either be a 'str' for keys or 'int' for index values
144
+ print(f"{', '.join([str(component) for component in data.path_components])}")
145
+ # $, items, 0, adddress, prefix-list, 0, prefix
146
+
147
+ # Access the found node.
148
+ print(f"{data.node}")
149
+ # 2001:db8::1/64
150
+ ```
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.12,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "simple_jsonpath"
7
+ requires-python = ">=3.10,<3.15"
8
+ classifiers = [
9
+ "Intended Audience :: Developers",
10
+ "Programming Language :: Rust",
11
+ "Programming Language :: Python :: Implementation :: CPython",
12
+ "Programming Language :: Python :: Implementation :: PyPy",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ ]
20
+ authors = [
21
+ { name = "Jeremy Spencer", email = "seojumper@gmail.com" },
22
+ ]
23
+ description = "A simple - yet quick - JSONPath implementation for querying JSON data."
24
+ readme = "README.md"
25
+ license = "MIT"
26
+ license-files = ["LICENSE"]
27
+ keywords = ["jsonpath"]
28
+
29
+ dynamic = ["version"]
30
+
31
+ [tool.maturin]
32
+ python-source = "python"
33
+ module-name = "simple_jsonpath._simple_jsonpath"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/seojumper/simple_jsonpath"
@@ -0,0 +1,7 @@
1
+
2
+ """A simple - yet quick - JSONPath implementation for querying JSON data."""
3
+
4
+ from .jsonpath import JsonPath, LocatedNode
5
+
6
+
7
+ __all__ = ["JsonPath", "LocatedNode"]
@@ -0,0 +1,16 @@
1
+ """A Python module for querying JSON data using JSONPath expressions."""
2
+
3
+
4
+ class SimpleJsonPath:
5
+ """A parser object that can be reused for multiple queries on the same JSON data."""
6
+ def __init__(self) -> None: ...
7
+ def set_data_from_json_str(self, input_data: str) -> None:
8
+ """Set the JSON data for the parser from a JSON string."""
9
+ ...
10
+ def find_from_set_data(self, path: str) -> str:
11
+ """Find the value(s) in the JSON data that match the given JSONPath expression, using a cache for parsed paths."""
12
+ ...
13
+
14
+ def find_location_from_set_data(self, path: str) -> str:
15
+ """Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations, using a cache for parsed paths."""
16
+ ...
@@ -0,0 +1,144 @@
1
+ from ._simple_jsonpath import SimpleJsonPath as RustSimpleJsonPath
2
+ from typing import Any, Union
3
+ from dataclasses import dataclass
4
+ import builtins
5
+ import json
6
+
7
+ class PathComponentsIter:
8
+ def __init__(self, path: str, nodes: list[tuple[int, int]]):
9
+ self._current: int = 0
10
+ self._path: str = path
11
+ self._end: int = len(nodes)
12
+ self._items: list[tuple[int,int]] = nodes
13
+
14
+ def __next__(self) -> Union[str, int]:
15
+
16
+ if self._current >= self._end:
17
+ raise StopIteration
18
+ if self._current == 0:
19
+ self._current +=1
20
+ return "$"
21
+ else:
22
+ item = self._items[self._current]
23
+ if item[0] == 0:
24
+ self._current += 1
25
+ return item[1]
26
+ else:
27
+ self._current += 1
28
+ return self._path[item[0]:item[1]]
29
+
30
+ @dataclass(frozen=True)
31
+ class PathComponents:
32
+ _path: str
33
+ _items: list[tuple[int, int]]
34
+
35
+ def __len__(self) -> int:
36
+ return len(self._items)
37
+
38
+ def __iter__(self) -> PathComponentsIter:
39
+ return PathComponentsIter(self._path, self._items)
40
+
41
+ def __getitem__(self, index: int) -> Union[str, int]:
42
+ if index >= len(self._items):
43
+ raise IndexError("Index out of range")
44
+ elif index == 0:
45
+ return "$"
46
+ elif index < 0:
47
+ raise IndexError("Negative indexing is not supported")
48
+ else:
49
+ item = self._items[index]
50
+ if item[0] == 0:
51
+ return item[1]
52
+ else:
53
+ return self._path[item[0]:item[1]]
54
+ def __contains__(self, item: Union[int, str]) -> bool:
55
+ if isinstance(item, int):
56
+ for i in range(len(self._items)):
57
+ if i == 0:
58
+ continue
59
+ else:
60
+ if self._items[i][0] == 0 and self._items[i][1] == item:
61
+ return True
62
+ return False
63
+ else:
64
+ for i in range(len(self._items[1:])):
65
+ if i == 0:
66
+ if item == "$":
67
+ return True
68
+ else:
69
+ if self._path[self._items[i][0]:self._items[i][1]] == item:
70
+ return True
71
+ return False
72
+
73
+ class LocatedNode:
74
+ """A struct to hold the located nodes found from a located JSONPath query."""
75
+ def __init__(self, full_path: str, path_components: list[tuple[int, int]], node: Union[str,int,float,bool,None,dict[str, Any], list[Any]]) -> None:
76
+ self._full_path: str = full_path
77
+ self._path_components: PathComponents = PathComponents(full_path, path_components)
78
+ self._node: Union[str,int,float,bool,None,dict[str, Any], list[Any]] = node
79
+
80
+ @builtins.property
81
+ def path_components(self) -> PathComponents:
82
+ """An iterator that yields the path components of the last query result."""
83
+ return self._path_components
84
+ @builtins.property
85
+ def full_path(self) -> str:
86
+ """The full path of the last query result."""
87
+ return self._full_path
88
+ @builtins.property
89
+ def node(self) -> Union[str,int,float,bool,None,dict[str, Any], list[Any]]:
90
+ """The node value of the last query result."""
91
+ return self._node
92
+
93
+
94
+ class JsonPath:
95
+ """A simple JSONPath implementation for querying JSON data.
96
+
97
+ It uses a Rust backend for performance and supports caching of parsed
98
+ JSONPath expressions for repeated queries against the same JSON data.
99
+ """
100
+ def __init__(self) -> None:
101
+ self._parser = RustSimpleJsonPath()
102
+
103
+ def set_data(self, input_data: Union[dict[str, Any], list[Any]]) -> None:
104
+ """Set the JSON data for the parser from a Python dictionary or list.
105
+
106
+ Args:
107
+ input_data: The JSON data to set, as a Python dictionary or list.
108
+
109
+ Returns:
110
+ None
111
+
112
+ Raises:
113
+ ValueError: If the input data is not a valid JSON object or array.
114
+ """
115
+ self._parser.set_data_from_json_str(json.dumps(input_data))
116
+
117
+ def find(self, path: str) -> list[Any]:
118
+ """Find the value(s) in the JSON data that match the given JSONPath expression.
119
+
120
+ Args:
121
+ path: The JSONPath expression to evaluate.
122
+
123
+ Returns:
124
+ A list of values that match the JSONPath expression.
125
+
126
+ Raises:
127
+ ValueError: If the JSONPath expression is invalid.
128
+ """
129
+ return json.loads(self._parser.find_from_set_data(path))
130
+
131
+ def find_located(self, path: str) -> list[LocatedNode]:
132
+ """Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations.
133
+
134
+ Args:
135
+ path: The JSONPath expression to evaluate.
136
+
137
+ Returns:
138
+ A list of LocatedNode objects that match the JSONPath expression.
139
+
140
+ Raises:
141
+ ValueError: If the JSONPath expression is invalid.
142
+ """
143
+ result = json.loads(self._parser.find_location_from_set_data(path))
144
+ return [LocatedNode(item['full_path'], item['path_components'], item['node']) for item in result]
File without changes
@@ -0,0 +1,289 @@
1
+
2
+
3
+ use pyo3::prelude::*;
4
+ use serde_json_path::NormalizedPath;
5
+ use serde_json::Value;
6
+ use serde::{Serialize};
7
+
8
+
9
+ /// A Python module for querying JSON data using JSONPath expressions.
10
+ #[pymodule]
11
+ #[pyo3(name = "_simple_jsonpath")]
12
+ mod simple_jsonpath {
13
+ use super::*;
14
+ use serde_json_path::JsonPath;
15
+ use serde_json::Value;
16
+ use std::{collections::HashMap, sync::Arc};
17
+
18
+ /// A parser object that can be reused for multiple queries on the same JSON data.
19
+ #[pyclass]
20
+ pub struct SimpleJsonPath {
21
+ inner: HashMap<String, JsonPath>,
22
+ data: Arc<Value>,
23
+ }
24
+
25
+ #[pymethods]
26
+ impl SimpleJsonPath {
27
+ /// Create a new SimpleJsonPath object with an empty cache and null data.
28
+ #[new]
29
+ pub fn new() -> PyResult<Self> {
30
+ Ok(Self { inner: HashMap::new(), data: Arc::new(Value::Null) })
31
+ }
32
+
33
+ /// Set the JSON data for the parser from a JSON string.
34
+ pub fn set_data_from_json_str(&mut self, input_data: &str) -> PyResult<()> {
35
+ let value: Value = serde_json::from_str(input_data).map_err(|e| {
36
+ pyo3::exceptions::PyValueError::new_err(format!("Invalid JSON string: {}", e))
37
+ })?;
38
+ self.data = Arc::new(value);
39
+ Ok(())
40
+ }
41
+
42
+ /// Find the value(s) in the JSON data that match the given JSONPath expression, using a cache for parsed paths.
43
+ pub fn find_from_set_data(&mut self, path: &str) -> PyResult<String> {
44
+ if !self.inner.contains_key(path) {
45
+ let json_path = JsonPath::parse(path).map_err(|e| {
46
+ pyo3::exceptions::PyValueError::new_err(format!(
47
+ "Parse error at position {} for path {}: {}",
48
+ e.position(),
49
+ path,
50
+ e.message()
51
+ ))
52
+ })?;
53
+ self.inner.insert(path.to_string(), json_path);
54
+ }
55
+ let json_path = self.inner.get(path).unwrap();
56
+ let result = json_path.query(&self.data).all();
57
+ let pyresult: String = serde_json::to_string(&result).map_err(|e| {
58
+ pyo3::exceptions::PyValueError::new_err(format!(
59
+ "Error converting result to JSON string: {}",
60
+ e
61
+ ))
62
+ })?;
63
+ Ok(pyresult)
64
+ }
65
+
66
+ /// Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations, using a cache for parsed paths.
67
+ pub fn find_location_from_set_data(
68
+ &mut self,
69
+ path: &str,
70
+ ) -> PyResult<String> {
71
+ if !self.inner.contains_key(path) {
72
+ let json_path = JsonPath::parse(path).map_err(|e| {
73
+ pyo3::exceptions::PyValueError::new_err(format!(
74
+ "Parse error at position {} for path {}: {}",
75
+ e.position(),
76
+ path,
77
+ e.message()
78
+ ))
79
+ })?;
80
+ self.inner.insert(path.to_string(), json_path);
81
+ }
82
+ let json_path = self.inner.get(path).unwrap();
83
+ let result = json_path.query_located(&self.data).all();
84
+ let mut located_nodes = Vec::new();
85
+ for item in &result {
86
+ located_nodes.push(LocatedNode::new(item.location(), item.node()));
87
+
88
+ }
89
+ let pyresult: String = serde_json::to_string(&located_nodes).map_err(|e| {
90
+ pyo3::exceptions::PyValueError::new_err(format!(
91
+ "Error converting result to JSON string: {}",
92
+ e
93
+ ))
94
+ })?;
95
+ Ok(pyresult)
96
+ }
97
+ }
98
+ }
99
+
100
+
101
+ /// A struct to hold the located nodes for serialization.
102
+ #[derive(Serialize)]
103
+ struct LocatedNode<'a> {
104
+ full_path: String,
105
+ path_components: Vec<(usize, usize)>,
106
+ node: &'a Value,
107
+ }
108
+
109
+ impl<'a> LocatedNode<'a> {
110
+ fn new(full_path: &'a NormalizedPath, node: &'a Value) -> LocatedNode<'a> {
111
+ let (full_path, path_components) = split_normalized_path_component_ranges(full_path);
112
+ LocatedNode { full_path, path_components, node }
113
+ }
114
+ }
115
+
116
+ /// Splits a normalized JSONPath into byte ranges `(start, end)` per component.
117
+ ///
118
+ /// Example: `$['items'][0]['name']` -> `[(0,1), (1,10), (10,13), (13,21)]`
119
+ fn split_normalized_path_component_ranges(path: &NormalizedPath) -> (String, Vec<(usize, usize)>) {
120
+ let path_str = path.to_string();
121
+ let chars = &path_str.as_str();
122
+
123
+ let mut ranges = Vec::with_capacity(path.len() + 1);
124
+ ranges.push((0, 1)); // Start with the root component '$'
125
+
126
+ #[derive(Debug)]
127
+ enum State {
128
+ Start,
129
+ Root,
130
+ InBracket,
131
+ InQuotedField,
132
+ InEscapedChar,
133
+ InIndex,
134
+ }
135
+ let mut num_start = None;
136
+ let mut start = None;
137
+ let mut state = State::Start;
138
+ for (i, c) in chars.char_indices() {
139
+ match state {
140
+ State::Start => {
141
+ if c == '$' {
142
+ state = State::Root;
143
+ }
144
+ }
145
+ State::Root => {
146
+ if c == '[' {
147
+ state = State::InBracket;
148
+ }
149
+ }
150
+ State::InBracket => {
151
+ if c == ']' {
152
+ state = State::Root;
153
+ } else if c == '\'' {
154
+ state = State::InQuotedField;
155
+ } else {
156
+ state = State::InIndex;
157
+ num_start = Some(i);
158
+ }
159
+ }
160
+ State::InQuotedField => {
161
+ match c {
162
+ '\\' => state = State::InEscapedChar,
163
+ '\'' => {
164
+ match start {
165
+ Some(_) =>
166
+ ranges.push((start.take().unwrap(), i)),
167
+ None => ranges.push((i+1, i-1)),
168
+ }
169
+ state = State::InBracket;
170
+ },
171
+ _ => {
172
+ match start {
173
+ Some(_) => {},
174
+ None => start = Some(i),
175
+ }
176
+ },
177
+ }
178
+ }
179
+ State::InEscapedChar => {
180
+ state = State::InQuotedField;
181
+ }
182
+ State::InIndex => {
183
+ if c == ']' {
184
+ if num_start.is_some() {
185
+ let num = chars[num_start.take().unwrap()..i].parse::<usize>().unwrap();
186
+ ranges.push((0,num));
187
+ }
188
+ state = State::Root;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ (path_str, ranges)
195
+ }
196
+
197
+ #[cfg(test)]
198
+ mod tests {
199
+ use super::split_normalized_path_component_ranges;
200
+ use super::simple_jsonpath::SimpleJsonPath;
201
+ use rstest::rstest;
202
+ use serde_json::Number;
203
+ use serde_json::Value;
204
+ use serde_json_path::{JsonPath};
205
+
206
+ #[rstest]
207
+ #[case("$.router.bgp[0].bgp.router_id.interface")]
208
+ #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.default.'ipv4-unicast'")]
209
+ #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'log-neighbor-changes'")]
210
+ #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'graceful-restart'")]
211
+ #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'update-delay'")]
212
+ #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].template.'peer-policy'[*]")]
213
+ #[case(
214
+ "$.router.'Cisco-IOS-XE-bgp:bgp'[0].'address-family'.'no-vrf'.ipv4[0].'ipv4-unicast'.'aggregate-address'[*]"
215
+ )]
216
+ fn path(#[case] input: &str) {
217
+ let result = JsonPath::parse(input);
218
+ match &result {
219
+ Ok(path) => {}
220
+ Err(e) => {
221
+ println!("Parse error at position {}: {}", e.position(), e.message());
222
+ }
223
+ }
224
+ assert!(result.is_ok());
225
+ }
226
+
227
+ #[test]
228
+ fn split_normalized_path_into_component_ranges() {
229
+ let data: Value = serde_json::from_str(r#"{"items":[{"name":"a"}] }"#).unwrap();
230
+ let path = JsonPath::parse("$.items[0].name").unwrap();
231
+ let located = path.query_located(&data).all();
232
+ let location = located.first().unwrap().location();
233
+ // $['items'][0]['name']
234
+ println!("Getting here");
235
+ let ranges = split_normalized_path_component_ranges(&location);
236
+ println!("Getting here 2");
237
+ let expected = vec![(0, 1), (3, 7), (0, 0), (15, 18)];
238
+ for range in &ranges.1 {
239
+ println!("Component: {}", &location.to_string()[range.0..range.1]);
240
+ }
241
+ println!("Getting here 3");
242
+
243
+ assert_eq!(ranges.1, expected);
244
+ }
245
+
246
+
247
+
248
+ // #[test]
249
+ // fn find_location() {
250
+ // let data = r#"[
251
+ // {"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}},
252
+ // {"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}}
253
+ // ]"#;
254
+ // let path = "$[*].name";
255
+ // let data: Value = serde_json::from_str(data).unwrap();
256
+ // let data_str = serde_json::to_string(&data).unwrap();
257
+ // let mut finder = SimpleJsonPath::new().unwrap();
258
+ // finder.set_data_from_json_str(&data_str).unwrap();
259
+ // let result: Value = serde_json::from_str(&finder.find_location_from_set_data(path).unwrap()).unwrap();
260
+ // assert_eq!(result.as_array().unwrap().len(), 2);
261
+ // let array = result.as_array().unwrap();
262
+ // let path_components_1 = Value::Array(vec![Value::String("$".to_string()), Value::Number(Number::from_str("0").unwrap()), Value::String("name".to_string())]);
263
+ // let path_components_2 = Value::Array(vec![Value::String("$".to_string()), Value::Number(Number::from_str("1").unwrap()), Value::String("name".to_string())]);
264
+ // assert_eq!(array.len(), 2);
265
+
266
+ // println!("{:?}", array);
267
+ // let located_node1 = array[0].as_object().unwrap();
268
+ // let full_path = located_node1.get("full_path").unwrap().as_str().unwrap();
269
+ // assert_eq!(full_path, " ");
270
+ // let path_components = located_node1.get("path_components").unwrap().as_array().unwrap();
271
+ // assert_eq!(path_components, path_components_1.as_array().unwrap());
272
+ // let nodes = located_node1.get("nodes").unwrap().as_array().unwrap();
273
+ // assert_eq!(nodes.len(), 1);
274
+
275
+
276
+ // let located_node2 = array[1].as_object().unwrap();
277
+ // let full_path = located_node2.get("full_path").unwrap().as_str().unwrap();
278
+ // assert_eq!(full_path, "$[1]['name']");
279
+ // let path_components = located_node2.get("path_components").unwrap().as_array().unwrap();
280
+ // assert_eq!(path_components, path_components_2.as_array().unwrap());
281
+ // let nodes = located_node2.get("nodes").unwrap().as_array().unwrap();
282
+ // assert_eq!(nodes.len(), 1);
283
+
284
+
285
+
286
+
287
+ // // assert_eq!(result.as_array().unwrap()[0].as_str().unwrap(), "1/0/1");
288
+ // }
289
+ }
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: simple_jsonpath
3
- Version: 0.1.2
4
- Classifier: Programming Language :: Rust
5
- Classifier: Programming Language :: Python :: Implementation :: CPython
6
- Classifier: Programming Language :: Python :: Implementation :: PyPy
7
- Requires-Python: >=3.8
@@ -1,7 +0,0 @@
1
- # simple_jsonpath
2
-
3
- ## Installation
4
-
5
- ```bash
6
- pip install simple_json
7
- ```
@@ -1,13 +0,0 @@
1
- [build-system]
2
- requires = ["maturin>=1.12,<2.0"]
3
- build-backend = "maturin"
4
-
5
- [project]
6
- name = "simple_jsonpath"
7
- requires-python = ">=3.8"
8
- classifiers = [
9
- "Programming Language :: Rust",
10
- "Programming Language :: Python :: Implementation :: CPython",
11
- "Programming Language :: Python :: Implementation :: PyPy",
12
- ]
13
- dynamic = ["version"]
@@ -1,6 +0,0 @@
1
- """A Python module for querying JSON data using JSONPath expressions."""
2
-
3
- from typing import Optional
4
-
5
- def find(path: str, data: object) -> Optional[str]:
6
- """Find the value(s) in the JSON data that match the given JSONPath expression."""
@@ -1,74 +0,0 @@
1
- use pyo3::{prelude::*};
2
- use serde_json_path::{JsonPath};
3
-
4
- /// A Python module for querying JSON data using JSONPath expressions.
5
- #[pymodule]
6
- mod simple_jsonpath {
7
- use super::*;
8
-
9
- /// Find the value(s) in the JSON data that match the given JSONPath expression.
10
- #[pyfunction]
11
- fn find(path: String, data: String) -> PyResult<Option<String>> {
12
- match serde_json::from_str(&data) {
13
- Ok(json) => {
14
- match JsonPath::parse(&path) {
15
- Ok(json_path) => {
16
- let result = json_path.query(&json).all();
17
- if result.is_empty() {
18
- Ok(None)
19
- } else {
20
- // Convert the result to a JSON string
21
- match serde_json::to_string(&result) {
22
- Ok(json_result) => Ok(Some(json_result)),
23
- Err(e) => return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Error converting result to JSON string: {}", e))),
24
- }
25
- }
26
- }
27
- Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Parse error at position {} for path {}:\n{}", e.position(), path, e.message()))),
28
- }
29
- }
30
- Err(e) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Invalid JSON data: {}", e))),
31
- }
32
- }
33
-
34
- }
35
-
36
- #[cfg(test)]
37
- mod tests {
38
- use super::*;
39
- use rstest::rstest;
40
- use serde_json_path::{JsonPath, ParseError};
41
- use serde_json::Value;
42
-
43
- #[rstest]
44
- #[case("$.router.bgp[0].bgp.router_id.interface")]
45
- #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.default.'ipv4-unicast'")]
46
- #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'log-neighbor-changes'")]
47
- #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'graceful-restart'")]
48
- #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].bgp.'update-delay'")]
49
- #[case("$.router.'Cisco-IOS-XE-bgp:bgp'[0].template.'peer-policy'[*]")]
50
- #[case(
51
- "$.router.'Cisco-IOS-XE-bgp:bgp'[0].'address-family'.'no-vrf'.ipv4[0].'ipv4-unicast'.'aggregate-address'[*]"
52
- )]
53
- fn path(#[case] input: &str) {
54
- let result = JsonPath::parse(input);
55
- match &result {
56
- Ok(path) => {}
57
- Err(e) => {
58
- println!("Parse error at position {}: {}", e.position(), e.message());
59
- }
60
- }
61
- assert!(result.is_ok());
62
- }
63
-
64
- #[test]
65
- fn find() {
66
- let data = r#"
67
- {"name": "1/0/1", "description": "Test Interface 1/0/1", "media-type": "sfp", "switchport-conf": {"switchport": false}, "arp": {"timeout": 1000}, "bfd": {"Cisco-IOS-XE-bfd:enable": true, "Cisco-IOS-XE-bfd:local-address": "10.100.1.10", "Cisco-IOS-XE-bfd:interval-interface": {"msecs": 300, "min_rx": 300, "multiplier": 3}, "Cisco-IOS-XE-bfd:echo": false}, "bandwidth": {"kilobits": 1000000}, "mpls": {"Cisco-IOS-XE-mpls:ip": [null], "Cisco-IOS-XE-mpls:mtu": 1400}, "vrf": {"forwarding": "PRODUCTION"}, "ip": {"access-group": {"in": {"acl": {"acl-name": "ACL_IN", "in": [null]}}, "out": {"acl": {"acl-name": "ACL_OUT", "out": [null]}}}, "arp": {"inspection": {"limit": {"rate": 1000}, "trust": [null]}}, "address": {"primary": {"address": "10.100.1.10", "mask": "255.255.255.0"}}, "Cisco-IOS-XE-nat:nat": {"inside": [null]}, "helper-address": [{"address": "10.100.1.50", "vrf": "PRODUCTION"}], "pim": {"Cisco-IOS-XE-multicast:border": [null], "Cisco-IOS-XE-multicast:bfd": [null], "Cisco-IOS-XE-multicast:pim-mode-choice-cfg": {"sparse-mode": {}}, "Cisco-IOS-XE-multicast:dr-priority": 1000}, "proxy-arp": false, "redirects": false, "dhcp": {"Cisco-IOS-XE-dhcp:relay": {"information": {"option": {"vpn-id": [null]}}, "source-interface": "Loopback150"}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST", "direction": "input"}]}, "Cisco-IOS-XE-icmp:unreachables": false, "Cisco-IOS-XE-igmp:igmp": {"version": 3}, "Cisco-IOS-XE-nbar:nbar": {"protocol-discovery": {}}, "Cisco-IOS-XE-ospf:router-ospf": {"ospf": {"process-id": [{"id": 10, "area": [{"area-id": 0}]}], "cost": 10, "dead-interval": 60, "hello-interval": 15, "mtu-ignore": true, "message-digest-key": [{"id": 1, "md5": {"auth-type": 0, "auth-key": "cisco"}}], "network": {"point-to-point": [null]}, "multi-area": {"multi-area-id": [{"area-id": 1}, {"area-id": 2}]}, "priority": 10, "ttl-security": {"hops": 5}}}}, "ipv6": {"address": {"prefix-list": [{"prefix": "2001:db8::1/64", "eui-64": [null]}], "link-local-address": [{"address": "fe80::1", "link-local": [null]}]}, "enable": [null], "mtu": 1600, "nd": {"Cisco-IOS-XE-nd:ra": {"suppress": {"all": [null]}}}, "Cisco-IOS-XE-flow:flow": {"monitor-new": [{"name": "MONITOR_C9K_TEST_V6", "direction": "input"}]}, "Cisco-IOS-XE-multicast:pim-conf": {"pim": true}, "Cisco-IOS-XE-multicast:pim-container": {"bfd": [null], "dr-priority": 1000}}, "load-interval": 30, "logging": {"event": {"link-status-enable": true}}, "mtu": 1600, "Cisco-IOS-XE-cdp:cdp": {"enable": true, "tlv": {"default-wrp": {"app": false}, "server-location-config": false, "location-config": false}}, "Cisco-IOS-XE-dot1x:dot1x": {"pae": "authenticator", "max-reauth-req": 3, "max-req": 3, "timeout": {"auth-period": 10000, "held-period": 40000, "quiet-period": 10000, "ratelimit-period": 5000, "server-timeout": 5000, "start-period": 2000, "supp-timeout": 2000, "tx-period": 2000}}, "Cisco-IOS-XE-ethernet:port-settings": {"speed": {"speed-value": "100"}, "auto-negotiation": "disable"}, "Cisco-IOS-XE-ethernet:speed": {"nonegotiate": [null]}, "Cisco-IOS-XE-ospfv3:ospfv3": {"cost-config": {"value": 10}, "network-type": {"point-to-point": [null]}}, "Cisco-IOS-XE-policy:service-policy": {"input": "POLICY_IN", "output": "POLICY_OUT"}, "Cisco-IOS-XE-sanet:authentication": {"periodic": [null], "timer": {"reauthenticate": {"server-config": [null]}}}, "Cisco-IOS-XE-sanet:mab": {"eap": [null]}, "Cisco-IOS-XE-snmp:snmp": {"trap": {"link-status": true}}}"#;
68
- let path = "$.name";
69
- let data: Value = serde_json::from_str(data).unwrap();
70
- let result = JsonPath::parse(path).unwrap().query(&data).all();
71
- assert_eq!(result.len(), 1);
72
- assert_eq!(result[0].as_str().unwrap(), "1/0/1");
73
- }
74
- }