simple_jsonpath 0.3.3__tar.gz → 0.4.3__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.
@@ -73,4 +73,5 @@ docs/_build/
73
73
 
74
74
  path.py
75
75
  device.json
76
- ben
76
+ ben
77
+ uv.lock
@@ -2,6 +2,18 @@
2
2
  # It is not intended for manual editing.
3
3
  version = 4
4
4
 
5
+ [[package]]
6
+ name = "ahash"
7
+ version = "0.8.12"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
10
+ dependencies = [
11
+ "cfg-if",
12
+ "once_cell",
13
+ "version_check",
14
+ "zerocopy",
15
+ ]
16
+
5
17
  [[package]]
6
18
  name = "aho-corasick"
7
19
  version = "1.1.4"
@@ -11,6 +23,18 @@ dependencies = [
11
23
  "memchr",
12
24
  ]
13
25
 
26
+ [[package]]
27
+ name = "bitflags"
28
+ version = "2.11.0"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
31
+
32
+ [[package]]
33
+ name = "byteorder"
34
+ version = "1.5.0"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
37
+
14
38
  [[package]]
15
39
  name = "cfg-if"
16
40
  version = "1.0.4"
@@ -108,12 +132,27 @@ version = "1.0.17"
108
132
  source = "registry+https://github.com/rust-lang/crates.io-index"
109
133
  checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
110
134
 
135
+ [[package]]
136
+ name = "lazy_static"
137
+ version = "1.5.0"
138
+ source = "registry+https://github.com/rust-lang/crates.io-index"
139
+ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
140
+
111
141
  [[package]]
112
142
  name = "libc"
113
143
  version = "0.2.183"
114
144
  source = "registry+https://github.com/rust-lang/crates.io-index"
115
145
  checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
116
146
 
147
+ [[package]]
148
+ name = "lock_api"
149
+ version = "0.4.14"
150
+ source = "registry+https://github.com/rust-lang/crates.io-index"
151
+ checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
152
+ dependencies = [
153
+ "scopeguard",
154
+ ]
155
+
117
156
  [[package]]
118
157
  name = "memchr"
119
158
  version = "2.8.0"
@@ -142,6 +181,29 @@ version = "1.21.4"
142
181
  source = "registry+https://github.com/rust-lang/crates.io-index"
143
182
  checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
144
183
 
184
+ [[package]]
185
+ name = "parking_lot"
186
+ version = "0.12.5"
187
+ source = "registry+https://github.com/rust-lang/crates.io-index"
188
+ checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
189
+ dependencies = [
190
+ "lock_api",
191
+ "parking_lot_core",
192
+ ]
193
+
194
+ [[package]]
195
+ name = "parking_lot_core"
196
+ version = "0.9.12"
197
+ source = "registry+https://github.com/rust-lang/crates.io-index"
198
+ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
199
+ dependencies = [
200
+ "cfg-if",
201
+ "libc",
202
+ "redox_syscall",
203
+ "smallvec",
204
+ "windows-link",
205
+ ]
206
+
145
207
  [[package]]
146
208
  name = "pin-project-lite"
147
209
  version = "0.2.17"
@@ -240,6 +302,15 @@ dependencies = [
240
302
  "proc-macro2",
241
303
  ]
242
304
 
305
+ [[package]]
306
+ name = "redox_syscall"
307
+ version = "0.5.18"
308
+ source = "registry+https://github.com/rust-lang/crates.io-index"
309
+ checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
310
+ dependencies = [
311
+ "bitflags",
312
+ ]
313
+
243
314
  [[package]]
244
315
  name = "regex"
245
316
  version = "1.12.3"
@@ -319,6 +390,12 @@ version = "1.0.22"
319
390
  source = "registry+https://github.com/rust-lang/crates.io-index"
320
391
  checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
321
392
 
393
+ [[package]]
394
+ name = "scopeguard"
395
+ version = "1.2.0"
396
+ source = "registry+https://github.com/rust-lang/crates.io-index"
397
+ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
398
+
322
399
  [[package]]
323
400
  name = "semver"
324
401
  version = "1.0.27"
@@ -416,7 +493,7 @@ dependencies = [
416
493
 
417
494
  [[package]]
418
495
  name = "simple_jsonpath"
419
- version = "0.3.3"
496
+ version = "0.4.3"
420
497
  dependencies = [
421
498
  "pyo3",
422
499
  "rstest",
@@ -424,6 +501,7 @@ dependencies = [
424
501
  "serde_json",
425
502
  "serde_json_path",
426
503
  "serde_json_path_core",
504
+ "ustr",
427
505
  ]
428
506
 
429
507
  [[package]]
@@ -432,6 +510,12 @@ version = "0.4.12"
432
510
  source = "registry+https://github.com/rust-lang/crates.io-index"
433
511
  checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
434
512
 
513
+ [[package]]
514
+ name = "smallvec"
515
+ version = "1.15.1"
516
+ source = "registry+https://github.com/rust-lang/crates.io-index"
517
+ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
518
+
435
519
  [[package]]
436
520
  name = "syn"
437
521
  version = "2.0.117"
@@ -505,6 +589,30 @@ version = "1.0.24"
505
589
  source = "registry+https://github.com/rust-lang/crates.io-index"
506
590
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
507
591
 
592
+ [[package]]
593
+ name = "ustr"
594
+ version = "1.1.0"
595
+ source = "registry+https://github.com/rust-lang/crates.io-index"
596
+ checksum = "18b19e258aa08450f93369cf56dd78063586adf19e92a75b338a800f799a0208"
597
+ dependencies = [
598
+ "ahash",
599
+ "byteorder",
600
+ "lazy_static",
601
+ "parking_lot",
602
+ ]
603
+
604
+ [[package]]
605
+ name = "version_check"
606
+ version = "0.9.5"
607
+ source = "registry+https://github.com/rust-lang/crates.io-index"
608
+ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
609
+
610
+ [[package]]
611
+ name = "windows-link"
612
+ version = "0.2.1"
613
+ source = "registry+https://github.com/rust-lang/crates.io-index"
614
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
615
+
508
616
  [[package]]
509
617
  name = "winnow"
510
618
  version = "0.7.15"
@@ -514,6 +622,26 @@ dependencies = [
514
622
  "memchr",
515
623
  ]
516
624
 
625
+ [[package]]
626
+ name = "zerocopy"
627
+ version = "0.8.47"
628
+ source = "registry+https://github.com/rust-lang/crates.io-index"
629
+ checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
630
+ dependencies = [
631
+ "zerocopy-derive",
632
+ ]
633
+
634
+ [[package]]
635
+ name = "zerocopy-derive"
636
+ version = "0.8.47"
637
+ source = "registry+https://github.com/rust-lang/crates.io-index"
638
+ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
639
+ dependencies = [
640
+ "proc-macro2",
641
+ "quote",
642
+ "syn",
643
+ ]
644
+
517
645
  [[package]]
518
646
  name = "zmij"
519
647
  version = "1.0.21"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "simple_jsonpath"
3
- version = "0.3.3"
3
+ version = "0.4.3"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -15,7 +15,7 @@ serde_json_path = { git = "https://github.com/seojumper/serde_json_path.git", br
15
15
  serde_json_path_core = { git = "https://github.com/seojumper/serde_json_path.git", branch = "quotes" }
16
16
  serde_json = "1.0.149"
17
17
  serde = { version = "1.0.228", features = ["derive"] }
18
-
18
+ ustr = "1.1.0"
19
19
 
20
20
  [dev-dependencies]
21
21
  rstest = "0.26.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_jsonpath
3
- Version: 0.3.3
3
+ Version: 0.4.3
4
4
  Classifier: Intended Audience :: Developers
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -31,7 +31,8 @@ pip install simple_jsonpath
31
31
 
32
32
  ## About
33
33
 
34
- This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
34
+ This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library that supports performing querying for data in a JSON document. It does **NOT** supporting modifying data
35
+ in place.
35
36
 
36
37
  ## Use
37
38
 
@@ -182,12 +183,16 @@ results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-lis
182
183
  for data in results:
183
184
 
184
185
  # Print the normalized full path where the node was found
185
- print(f"{data.full_path}")
186
+ print(f"{data.path}")
186
187
  # $['items'][0]['address']['prefix-list'][0]['prefix']
187
188
 
189
+ # Print the normalized full path of the parent where the node was found
190
+ print(f"{data.parent_path}")
191
+ # $['items'][0]['address']['prefix-list'][0]
192
+
188
193
  # Iterate over the components of the found path
189
194
  # Returned elements will either be a 'str' for keys or 'int' for index values
190
- print(f"{', '.join([str(component) for component in data.path_components])}")
195
+ print(f"{', '.join([str(component) for component in data.path])}")
191
196
  # $, items, 0, adddress, prefix-list, 0, prefix
192
197
 
193
198
  # Access the found node.
@@ -8,7 +8,8 @@ pip install simple_jsonpath
8
8
 
9
9
  ## About
10
10
 
11
- This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library.
11
+ This module is a JSONPath [RFC9535 - JSONPath: Query Expressions for JSON](https://datatracker.ietf.org/doc/html/rfc9535) utility library that supports performing querying for data in a JSON document. It does **NOT** supporting modifying data
12
+ in place.
12
13
 
13
14
  ## Use
14
15
 
@@ -159,12 +160,16 @@ results: list[LocatedNode] = finder.find_located("$.items[*].address.'prefix-lis
159
160
  for data in results:
160
161
 
161
162
  # Print the normalized full path where the node was found
162
- print(f"{data.full_path}")
163
+ print(f"{data.path}")
163
164
  # $['items'][0]['address']['prefix-list'][0]['prefix']
164
165
 
166
+ # Print the normalized full path of the parent where the node was found
167
+ print(f"{data.parent_path}")
168
+ # $['items'][0]['address']['prefix-list'][0]
169
+
165
170
  # Iterate over the components of the found path
166
171
  # Returned elements will either be a 'str' for keys or 'int' for index values
167
- print(f"{', '.join([str(component) for component in data.path_components])}")
172
+ print(f"{', '.join([str(component) for component in data.path])}")
168
173
  # $, items, 0, adddress, prefix-list, 0, prefix
169
174
 
170
175
  # Access the found node.
@@ -0,0 +1,5 @@
1
+ """A simple - yet quick - JSONPath implementation for querying JSON data."""
2
+
3
+ from .jsonpath import JsonPath
4
+
5
+ __all__ = ["JsonPath"]
@@ -1,11 +1,24 @@
1
1
  """A Python module for querying JSON data using JSONPath expressions."""
2
2
 
3
- from typing import Any
3
+ from typing import Any, Union, Optional
4
+
5
+ class Path:
6
+ """Object that represents the JSONPath where a node was found."""
7
+ @classmethod
8
+ def parent_path(cls) -> Optional[Path]:
9
+ """Returns the parent path to this path item.
10
+
11
+ Returns:
12
+ Optional[Path]
13
+ """
14
+ ...
15
+ def __getitem__(self, index: int) -> Union[int, str]: ...
16
+ def __str__(self) -> str: ...
17
+ def __repr__(self) -> str: ...
4
18
 
5
19
  class SimpleJsonPath:
6
20
  """A parser object that can be reused for multiple queries on the same JSON data."""
7
21
  def __init__(self) -> None: ...
8
-
9
22
  def child(self, value: str) -> SimpleJsonPath:
10
23
  """Spawn a child instance fo the Parser"""
11
24
  ...
@@ -15,10 +28,9 @@ class SimpleJsonPath:
15
28
  def set_data(self, input_data: bytes) -> None:
16
29
  """Set the JSON data for the parser from a JSON string."""
17
30
  ...
18
- def find(self, path: str) -> list[Any]:
31
+ def find(self, path: str) -> list[Any]:
19
32
  """Find the value(s) in the JSON data that match the given JSONPath expression, using a cache for parsed paths."""
20
33
  ...
21
-
22
- def find_located(self, path: str) -> list[Any]:
34
+ def find_located(self, path: str) -> list[tuple[Path, Any]]:
23
35
  """Find the value(s) in the JSON data that match the given JSONPath expression, along with their locations, using a cache for parsed paths."""
24
36
  ...
@@ -1,110 +1,54 @@
1
- from ._simple_jsonpath import SimpleJsonPath as RustSimpleJsonPath
1
+ from ._simple_jsonpath import SimpleJsonPath as RustSimpleJsonPath, Path
2
2
  import sys
3
+
3
4
  if sys.version_info >= (3, 11):
4
- from typing import Self, Union, Any
5
+ from typing import Self, Union, Any, Optional
5
6
  else:
6
- from typing_extensions import Self, Union, Any
7
+ from typing_extensions import Self, Union, Any, Optional
7
8
  import orjson
8
- from dataclasses import dataclass
9
9
  import builtins
10
- import json
11
-
12
- class PathComponentsIter:
13
- def __init__(self, path: str, nodes: list[tuple[int, int]]):
14
- self._current: int = 0
15
- self._path: str = path
16
- self._end: int = len(nodes)
17
- self._items: list[tuple[int,int]] = nodes
18
-
19
- def __next__(self) -> Union[str, int]:
20
- if self._current >= self._end:
21
- raise StopIteration
22
- if self._current == 0:
23
- self._current +=1
24
- return "$"
25
- else:
26
- item = self._items[self._current]
27
- if item[0] == 0:
28
- self._current += 1
29
- return item[1]
30
- else:
31
- self._current += 1
32
- return self._path[item[0]:item[1]]
33
-
34
- @dataclass(frozen=True)
35
- class PathComponents:
36
- _path: str
37
- _items: list[tuple[int, int]]
38
-
39
- def __len__(self) -> int:
40
- return len(self._items)
41
-
42
- def __iter__(self) -> PathComponentsIter:
43
- return PathComponentsIter(self._path, self._items)
44
-
45
- def __getitem__(self, index: int) -> Union[str, int]:
46
- if index >= len(self._items):
47
- raise IndexError("Index out of range")
48
- elif index == 0:
49
- return "$"
50
- elif index < 0:
51
- raise IndexError("Negative indexing is not supported")
52
- else:
53
- item = self._items[index]
54
- if item[0] == 0:
55
- return item[1]
56
- else:
57
- return self._path[item[0]:item[1]]
58
- def __contains__(self, item: Union[int, str]) -> bool:
59
- if isinstance(item, int):
60
- for i in range(len(self._items)):
61
- if i == 0:
62
- continue
63
- else:
64
- if self._items[i][0] == 0 and self._items[i][1] == item:
65
- return True
66
- return False
67
- else:
68
- for i in range(len(self._items[1:])):
69
- if i == 0:
70
- if item == "$":
71
- return True
72
- else:
73
- if self._path[self._items[i][0]:self._items[i][1]] == item:
74
- return True
75
- return False
76
-
10
+
11
+
77
12
  class LocatedNode:
78
- """A struct to hold the located nodes found from a located JSONPath query."""
79
- 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:
80
- self._full_path: str = full_path
81
- self._path_components: PathComponents = PathComponents(full_path, path_components)
82
- self._node: Union[str,int,float,bool,None,dict[str, Any], list[Any]] = node
13
+ """A class that represents a node found in the JSON data along with its location path."""
14
+
15
+ def __init__(
16
+ self,
17
+ path: Path,
18
+ node: Union[str, int, float, bool, None, dict[str, Any], list[Any]],
19
+ ) -> None:
20
+ self._path: Path = path
21
+ self._node: Union[str, int, float, bool, None, dict[str, Any], list[Any]] = node
83
22
 
84
23
  @builtins.property
85
- def path_components(self) -> PathComponents:
86
- """An iterator that yields the path components of the last query result."""
87
- return self._path_components
24
+ def path(self) -> Path:
25
+ """An iterator that yields the path components of the last query result.
26
+
27
+ The full path can be converted to a str through the str() method against this object.
28
+ """
29
+ return self._path
30
+
88
31
  @builtins.property
89
- def full_path(self) -> str:
90
- """The full path of the last query result."""
91
- return self._full_path
92
- @builtins.property
93
- def node(self) -> Union[str,int,float,bool,None,dict[str, Any], list[Any]]:
32
+ def node(self) -> Union[str, int, float, bool, None, dict[str, Any], list[Any]]:
94
33
  """The node value of the last query result."""
95
34
  return self._node
35
+
36
+ @builtins.property
37
+ def parent_path(self) -> Optional[Path]:
38
+ """The parent path of the last query result."""
39
+ return self._path.parent_path()
96
40
 
97
41
 
98
42
  class JsonPath:
99
43
  """A simple JSONPath implementation for querying JSON data.
100
-
44
+
101
45
  It uses a Rust backend for performance and supports caching of parsed
102
46
  JSONPath expressions for repeated queries against the same JSON data.
103
47
  """
48
+
104
49
  def __init__(self) -> None:
105
50
  self._parser = RustSimpleJsonPath()
106
51
 
107
-
108
52
  def child(self) -> Self:
109
53
  """Spawns a child instance of the class.
110
54
 
@@ -112,16 +56,16 @@ class JsonPath:
112
56
  to set_data() need to be called on it for it to function.
113
57
 
114
58
  It does however retain shared mutable access to the parent's collection
115
- of pre-parsed path objects across all spawned children.
59
+ of pre-parsed path objects across all spawned children.
116
60
 
117
61
  This is useful for the pattern of:
118
62
  1. Searching a document for a path query.
119
63
  2. Then using those results returned as the basis of a new 'root element'
120
64
  for 'deeper' searches into a document.
121
-
122
- Instead of assigning the original query results to current instance, it can
65
+
66
+ Instead of assigning the original query results to current instance, it can
123
67
  be beneficial to spawn a child for each result, and assign the result
124
- data to the child or multiple children if more than one result was returned.
68
+ data to the child or multiple children if more than one result was returned.
125
69
 
126
70
  With this pattern the 'base' parent object will automatically contain
127
71
  all parsed paths for the document that were searched by
@@ -134,10 +78,10 @@ class JsonPath:
134
78
  child_cls = self.__class__
135
79
  child = child_cls()
136
80
  return child
137
-
81
+
138
82
  def has_data(self) -> bool:
139
83
  """Returns True if this instance has data set to it, False otherwise."""
140
- return self._parser.has_data()
84
+ return self._parser.has_data()
141
85
 
142
86
  def set_data(self, input_data: Union[dict[str, Any], list[Any]]) -> None:
143
87
  """Set the JSON data for the query engine from a Python object.
@@ -146,7 +90,7 @@ class JsonPath:
146
90
  operations performed against this instance.
147
91
 
148
92
  Calling the function consecutively will replace any existing 'set' data.
149
-
93
+
150
94
  Args:
151
95
  input_data: The JSON data to set, as a Python dictionary or list.
152
96
 
@@ -163,19 +107,21 @@ class JsonPath:
163
107
 
164
108
  The path expression is first parsed, then executed against the data previously
165
109
  'set'. Parsed path expressions are cached for efficient future use.
166
-
110
+
167
111
  Args:
168
- path: The JSONPath expression to evaluate.
112
+ path(str): The JSONPath expression to evaluate.
169
113
 
170
114
  Returns:
171
- A list of values that match the JSONPath expression.
115
+ list[Any]: A list of values that match the JSONPath expression.
172
116
 
173
117
  Raises:
174
118
  ValueError: If the JSONPath expression is invalid.
175
119
  LookupError: If this is called before data has not been set to this object through 'set_data()'.
176
120
  """
177
121
  if not self._parser.has_data():
178
- raise LookupError("Data must be set through calling 'set_data()' before attempting a query")
122
+ raise LookupError(
123
+ "Data must be set through calling 'set_data()' before attempting a query"
124
+ )
179
125
  return self._parser.find(path)
180
126
 
181
127
  def find_located(self, path: str) -> list[LocatedNode]:
@@ -183,19 +129,20 @@ class JsonPath:
183
129
 
184
130
  The path expression is first parsed, then executed against the data previously
185
131
  'set'. Parsed path expressions are cached for efficient future use.
186
-
132
+
187
133
  Args:
188
- path: The JSONPath expression to evaluate.
134
+ path(str): The JSONPath expression to evaluate.
189
135
 
190
136
  Returns:
191
- A list of LocatedNode objects that match the JSONPath expression.
137
+ list[LocatedNode]: A list of 'LocatedNode' objects that match the JSONPath expression.
192
138
 
193
139
  Raises:
194
140
  ValueError: If the JSONPath expression is invalid.
195
141
  LookupError: If this is called before data has not been set to this object through 'set_data()'.
196
142
  """
197
143
  if not self._parser.has_data():
198
- raise LookupError("Data must be set through calling 'set_data()' before attempting a query")
144
+ raise LookupError(
145
+ "Data must be set through calling 'set_data()' before attempting a query"
146
+ )
199
147
  result = self._parser.find_located(path)
200
- return [LocatedNode(item['full_path'], item['path_components'], item['node']) for item in result]
201
-
148
+ return [LocatedNode(path, node) for (path, node) in result]
@@ -1,23 +1,25 @@
1
+ //! Module that provides a Python interface for querying JSON data using JSONPath expressions, implemented in Rust for performance.
2
+
1
3
  use pyo3::{
2
4
  exceptions,
3
5
  prelude::*,
4
- types::{PyBool, PyDict, PyDictMethods, PyFloat, PyInt, PyList, PyListMethods, PyString},
6
+ types::{PyBool, PyDict, PyDictMethods, PyFloat, PyList, PyListMethods, PyString},
5
7
  };
6
8
  use serde_json_path::NormalizedPath;
7
- // use simple_jsonpath::SimpleJsonPath;
8
9
 
9
10
  /// A Python module for querying JSON data using JSONPath expressions.
10
11
  #[pymodule]
11
12
  #[pyo3(name = "_simple_jsonpath")]
12
13
  mod simple_jsonpath {
13
14
  use super::*;
14
- use pyo3::types::PyBytes;
15
+ use pyo3::types::{PyBytes, PyInt, PySlice};
15
16
  use serde_json::Value;
16
17
  use serde_json_path::JsonPath;
17
18
  use std::{
18
19
  collections::HashMap,
19
20
  sync::{Arc, Mutex},
20
21
  };
22
+ use ustr::Ustr;
21
23
 
22
24
  /// A parser object that can be reused for multiple queries on the same JSON data.
23
25
  #[pyclass]
@@ -32,7 +34,7 @@ mod simple_jsonpath {
32
34
  #[new]
33
35
  pub fn new() -> PyResult<Self> {
34
36
  Ok(Self {
35
- inner: Arc::new(Mutex::new(HashMap::new())),
37
+ inner: Arc::new(Mutex::new(HashMap::with_capacity(500))),
36
38
  data: None,
37
39
  })
38
40
  }
@@ -53,6 +55,7 @@ mod simple_jsonpath {
53
55
  })
54
56
  }
55
57
 
58
+ /// Check if the parser has JSON data set for querying.
56
59
  pub fn has_data(&self) -> bool {
57
60
  self.data.is_some()
58
61
  }
@@ -130,14 +133,147 @@ mod simple_jsonpath {
130
133
  };
131
134
  let pyresult = PyList::empty(py);
132
135
  for item in &result {
133
- pyresult.append(
134
- LocatedNode::new(item.location(), item.node()).convert_to_py_any(py)?,
135
- )?;
136
+ pyresult.append((
137
+ Path::new(item.location()),
138
+ serialize_value(py, item.node())?,
139
+ ))?;
136
140
  }
137
141
 
138
142
  Ok(pyresult)
139
143
  }
140
144
  }
145
+ /// An enum to represent either a string key or an integer index in a JSONPath segment.
146
+ #[derive(Clone, Copy)]
147
+ enum Index {
148
+ U(Ustr),
149
+ I(usize),
150
+ }
151
+
152
+ impl Index {
153
+ fn to_normalized_segment(&self) -> String {
154
+ match self {
155
+ Self::I(num) => format!("[{num}]"),
156
+ Self::U(u) => format!("['{}']", u.as_str()),
157
+ }
158
+ }
159
+ }
160
+ /// A struct to represent a JSONPath location as a sequence of indexes, which can be accessed from Python.
161
+ #[pyclass(sequence)]
162
+ struct Path {
163
+ indexes: Vec<Index>,
164
+ }
165
+
166
+ #[pymethods]
167
+ impl Path {
168
+ /// Return the segment of the path at the given index, where the index can be an integer (positive or negative).
169
+ /// If the index is out of range, raise an IndexError. If the index is a slice, raise a ValueError since slicing is not supported.
170
+ fn __getitem__<'py>(&self, index: Bound<'py, PyAny>) -> PyResult<Bound<'py, PyAny>> {
171
+ let py = index.py();
172
+ match index.cast::<PyInt>() {
173
+ Ok(index) => {
174
+ let mut i = index.extract::<isize>()?;
175
+ if i < 0 {
176
+ if i == -1 {
177
+ let last = self.indexes.last().unwrap();
178
+ match last {
179
+ Index::U(u) => Ok(PyString::new(py, u.as_str()).into_any()),
180
+ Index::I(i) => Ok(PyInt::new(py, i).into_any()),
181
+ }
182
+ } else {
183
+ let abs_index = i.unsigned_abs();
184
+ if abs_index > self.__len__() {
185
+ Err(exceptions::PyIndexError::new_err("Index out of Range"))
186
+ } else if abs_index == self.__len__() {
187
+ Ok(PyString::new(py, "$").into_any())
188
+ } else {
189
+ let found =
190
+ self.indexes.get(self.indexes.len() - abs_index).unwrap();
191
+ match found {
192
+ Index::U(u) => Ok(PyString::new(py, u.as_str()).into_any()),
193
+ Index::I(i) => Ok(PyInt::new(py, i).into_any()),
194
+ }
195
+ }
196
+ }
197
+ } else if i as usize == 0 {
198
+ Ok(PyString::new(py, "$").into_any())
199
+ } else {
200
+ i -= 1;
201
+ if let Some(i) = self.indexes.get(i as usize) {
202
+ match i {
203
+ Index::U(u) => Ok(PyString::new(py, u.as_str()).into_any()),
204
+ Index::I(i) => Ok(PyInt::new(py, i).into_any()),
205
+ }
206
+ } else {
207
+ Err(exceptions::PyIndexError::new_err("Index out of Range"))
208
+ }
209
+ }
210
+ }
211
+ Err(_) => match index.cast::<PySlice>() {
212
+ Ok(_) => Err(exceptions::PyValueError::new_err(
213
+ "Slicing operations are not supported",
214
+ )),
215
+ Err(e) => Err(e.into()),
216
+ },
217
+ }
218
+ }
219
+
220
+ /// Return the number of segments in the path, which is one more than the number of indexes (to account for the root '$' segment).
221
+ fn __len__(&self) -> usize {
222
+ self.indexes.len() + 1
223
+ }
224
+ fn __repr__(&self) -> String {
225
+ if self.indexes.len() == 1 {
226
+ "$".to_string()
227
+ } else {
228
+ let mut string = "$".to_string();
229
+ string.extend(self.indexes[1..].iter().map(|i| match i {
230
+ Index::U(u) => format!("['{}']", u),
231
+ Index::I(num) => format!("[{num}]"),
232
+ }));
233
+ string
234
+ }
235
+ }
236
+ /// Return a string representation of the path, which is the same as the __repr__ method in this case.
237
+ fn __str__(&self) -> String {
238
+ if self.indexes.len() == 1 {
239
+ "$".to_string()
240
+ } else {
241
+ let mut string = "$".to_string();
242
+ string.extend(self.indexes.iter().map(|i| i.to_normalized_segment()));
243
+ string
244
+ }
245
+ }
246
+ /// Return a new Path object that represents the parent path of the current path, which is the same path without the last segment. If the current path is the root '$', return None.
247
+ fn parent_path(&self) -> Option<Path> {
248
+ if !self.indexes.is_empty() {
249
+ let mut path = Vec::with_capacity(self.__len__() - 1);
250
+ path.extend(
251
+ self.indexes[..self.indexes.len() - 1]
252
+ .iter()
253
+ .map(|index| *index),
254
+ );
255
+ Some(Path { indexes: path })
256
+ } else {
257
+ None
258
+ }
259
+ }
260
+ }
261
+
262
+ impl Path {
263
+ /// Create a new Path object from a NormalizedPath, which is a sequence of PathElements. Each PathElement can be either a Name (string key) or an Index (integer index), and we convert them into our Index enum to store in the Path struct.
264
+ fn new(location: &NormalizedPath) -> Self {
265
+ let ids: Vec<Index> = location
266
+ .iter()
267
+ .map(|item| match item {
268
+ serde_json_path::PathElement::Name(name) => Index::U(Ustr::from(name)),
269
+ serde_json_path::PathElement::Index(num) => Index::I(*num),
270
+ })
271
+ .collect();
272
+ Self { indexes: ids }
273
+ }
274
+ }
275
+
276
+ /// Helper function to serialize a serde_json::Value into a corresponding Python object.
141
277
  fn serialize_value<'a>(py: Python<'a>, value: &'a Value) -> PyResult<Bound<'a, PyAny>> {
142
278
  match value {
143
279
  Value::Null => Ok(py.None().into_bound(py)),
@@ -167,125 +303,14 @@ mod simple_jsonpath {
167
303
  }
168
304
  }
169
305
  }
170
-
171
- struct LocatedNode<'a> {
172
- full_path: String,
173
- path_components: Vec<(usize, usize)>,
174
- node: &'a Value,
175
- }
176
-
177
- impl<'a> LocatedNode<'a> {
178
- fn new(full_path: &NormalizedPath, node: &'a Value) -> Self {
179
- let (full_path, path_components) = split_normalized_path_component_ranges(full_path);
180
- LocatedNode {
181
- full_path,
182
- path_components,
183
- node,
184
- }
185
- }
186
- // Implement the conversion function
187
- fn convert_to_py_any<'py>(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
188
- // Convert the inner value to a Python integer;
189
- let dict = PyDict::new(py);
190
- let mapping = dict.as_mapping();
191
- mapping.set_item("full_path", self.full_path)?;
192
- mapping.set_item("path_components", self.path_components)?;
193
- mapping.set_item("node", serialize_value(py, self.node)?)?;
194
-
195
- Ok(dict.into_any())
196
- }
197
- }
198
-
199
- pub fn split_normalized_path_component_ranges(
200
- path: &NormalizedPath,
201
- ) -> (String, Vec<(usize, usize)>) {
202
- let path_str = path.to_string();
203
- let chars = &path_str.as_str();
204
-
205
- let mut ranges = Vec::with_capacity(path.len() + 1);
206
- ranges.push((0, 1)); // Start with the root component '$'
207
-
208
- #[derive(Debug)]
209
- enum State {
210
- Start,
211
- Root,
212
- InBracket,
213
- InQuotedField,
214
- InEscapedChar,
215
- InIndex,
216
- }
217
- let mut num_start = None;
218
- let mut start = None;
219
- let mut state = State::Start;
220
- for (i, c) in chars.char_indices() {
221
- match state {
222
- State::Start => {
223
- if c == '$' {
224
- state = State::Root;
225
- }
226
- }
227
- State::Root => {
228
- if c == '[' {
229
- state = State::InBracket;
230
- }
231
- }
232
- State::InBracket => {
233
- if c == ']' {
234
- state = State::Root;
235
- } else if c == '\'' {
236
- state = State::InQuotedField;
237
- } else {
238
- state = State::InIndex;
239
- num_start = Some(i);
240
- }
241
- }
242
- State::InQuotedField => match c {
243
- '\\' => state = State::InEscapedChar,
244
- '\'' => {
245
- match start {
246
- Some(_) => ranges.push((start.take().unwrap(), i)),
247
- None => ranges.push((i + 1, i - 1)),
248
- }
249
- state = State::InBracket;
250
- }
251
- _ => match start {
252
- Some(_) => {}
253
- None => start = Some(i),
254
- },
255
- },
256
- State::InEscapedChar => {
257
- state = State::InQuotedField;
258
- }
259
- State::InIndex => {
260
- if c == ']' {
261
- if num_start.is_some() {
262
- let num = chars[num_start.take().unwrap()..i]
263
- .parse::<usize>()
264
- .unwrap();
265
- ranges.push((0, num));
266
- }
267
- state = State::Root;
268
- }
269
- }
270
- }
271
- }
272
-
273
- (path_str, ranges)
274
- }
275
306
  }
276
307
 
277
- /// A struct to hold the located nodes for serialization.
278
308
 
279
- /// Splits a normalized JSONPath into byte ranges `(start, end)` per component.
280
- ///
281
- /// Example: `$['items'][0]['name']` -> `[(0,1), (1,10), (10,13), (13,21)]`
282
309
 
283
310
  #[cfg(test)]
284
311
  mod tests {
285
312
  // use super::simple_jsonpath::SimpleJsonPath;
286
- use super::simple_jsonpath::split_normalized_path_component_ranges;
287
313
  use rstest::rstest;
288
- use serde_json::Value;
289
314
  use serde_json_path::JsonPath;
290
315
 
291
316
  #[rstest]
@@ -309,22 +334,22 @@ mod tests {
309
334
  assert!(result.is_ok());
310
335
  }
311
336
 
312
- #[test]
313
- fn split_normalized_path_into_component_ranges() {
314
- let data: Value = serde_json::from_str(r#"{"items":[{"name":"a"}] }"#).unwrap();
315
- let path = JsonPath::parse("$.items[0].name").unwrap();
316
- let located = path.query_located(&data).all();
317
- let location = located.first().unwrap().location();
318
- // $['items'][0]['name']
319
- println!("Getting here");
320
- let ranges = split_normalized_path_component_ranges(&location);
321
- println!("Getting here 2");
322
- let expected = vec![(0, 1), (3, 7), (0, 0), (15, 18)];
323
- for range in &ranges.1 {
324
- println!("Component: {}", &location.to_string()[range.0..range.1]);
325
- }
326
- println!("Getting here 3");
337
+ // #[test]
338
+ // fn split_normalized_path_into_component_ranges() {
339
+ // let data: Value = serde_json::from_str(r#"{"items":[{"name":"a"}] }"#).unwrap();
340
+ // let path = JsonPath::parse("$.items[0].name").unwrap();
341
+ // let located = path.query_located(&data).all();
342
+ // let location = located.first().unwrap().location();
343
+ // // $['items'][0]['name']
344
+ // println!("Getting here");
345
+ // let ranges = split_normalized_path_component_ranges(&location);
346
+ // println!("Getting here 2");
347
+ // let expected = vec![(0, 1), (3, 7), (0, 0), (15, 18)];
348
+ // for range in &ranges.1 {
349
+ // println!("Component: {}", &location.to_string()[range.0..range.1]);
350
+ // }
351
+ // println!("Getting here 3");
327
352
 
328
- assert_eq!(ranges.1, expected);
329
- }
353
+ // assert_eq!(ranges.1, expected);
354
+ // }
330
355
  }
@@ -1,7 +0,0 @@
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"]
File without changes