json_p3 0.2.1
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +14 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +7 -0
- data/LICENCE +21 -0
- data/README.md +353 -0
- data/Rakefile +23 -0
- data/Steepfile +27 -0
- data/lib/json_p3/cache.rb +40 -0
- data/lib/json_p3/environment.rb +76 -0
- data/lib/json_p3/errors.rb +49 -0
- data/lib/json_p3/filter.rb +426 -0
- data/lib/json_p3/function.rb +16 -0
- data/lib/json_p3/function_extensions/count.rb +15 -0
- data/lib/json_p3/function_extensions/length.rb +17 -0
- data/lib/json_p3/function_extensions/match.rb +62 -0
- data/lib/json_p3/function_extensions/pattern.rb +39 -0
- data/lib/json_p3/function_extensions/search.rb +44 -0
- data/lib/json_p3/function_extensions/value.rb +15 -0
- data/lib/json_p3/lexer.rb +420 -0
- data/lib/json_p3/node.rb +42 -0
- data/lib/json_p3/parser.rb +553 -0
- data/lib/json_p3/path.rb +42 -0
- data/lib/json_p3/segment.rb +102 -0
- data/lib/json_p3/selector.rb +285 -0
- data/lib/json_p3/token.rb +74 -0
- data/lib/json_p3/unescape.rb +112 -0
- data/lib/json_p3/version.rb +5 -0
- data/lib/json_p3.rb +17 -0
- data/performance/benchmark.rb +33 -0
- data/performance/benchmark_ips.rb +29 -0
- data/performance/benchmark_small_citylots.rb +18 -0
- data/performance/memory_profile.rb +19 -0
- data/performance/memory_profile_small_citylots.rb +14 -0
- data/performance/profile.rb +30 -0
- data/sig/json_p3.rbs +1058 -0
- data.tar.gz.sig +1 -0
- metadata +110 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 61e9c2e2224055046c93b4aff7c4f12cfaed22805507d0678e23e322a7d2ee60
|
4
|
+
data.tar.gz: b3460ee7b36ab677af85540b5962701c16313723a98bfba7a722c9ebf7aa0d52
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5aba27e4070700def8c89ff758a244ebba2fc3f31fbeb882492b0bf9260c61b1f55be0bb655dc5885bc226b7a29f3640ddf78c3801eaabfa291b3937d8723496
|
7
|
+
data.tar.gz: 96df0d295a541d98e17100a72f10584780a023baef88e9141c108c0137456bc8f1524a5a3e627dc7c14db4368c107d69d7476c47271914a137e98bf26e8210c9
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-minitest
|
3
|
+
- rubocop-rake
|
4
|
+
- rubocop-performance
|
5
|
+
|
6
|
+
AllCops:
|
7
|
+
TargetRubyVersion: 3.0
|
8
|
+
NewCops: enable
|
9
|
+
|
10
|
+
Style/StringLiterals:
|
11
|
+
EnforcedStyle: double_quotes
|
12
|
+
|
13
|
+
Style/StringLiteralsInInterpolation:
|
14
|
+
EnforcedStyle: double_quotes
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 James Prior
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
<h1 align="center">JSONPath, JSON Patch and JSON Pointer for Ruby</h1>
|
2
|
+
|
3
|
+
<p align="center">
|
4
|
+
We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> strictly and test against the <a href="https://github.com/jsonpath-standard/jsonpath-compliance-test-suite">JSONPath Compliance Test Suite</a>.
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<p align="center">
|
8
|
+
<a href="https://github.com/jg-rp/ruby-json-p3/blob/main/LICENSE.txt">
|
9
|
+
<img src="https://img.shields.io/pypi/l/jsonpath-rfc9535.svg?style=flat-square" alt="License">
|
10
|
+
</a>
|
11
|
+
<a href="https://github.com/jg-rp/ruby-json-p3/actions">
|
12
|
+
<img src="https://img.shields.io/github/actions/workflow/status/jg-rp/ruby-json-p3/main.yml?branch=main&label=tests&style=flat-square" alt="Tests">
|
13
|
+
</a>
|
14
|
+
</p>
|
15
|
+
|
16
|
+
---
|
17
|
+
|
18
|
+
**Table of Contents**
|
19
|
+
|
20
|
+
- [Install](#install)
|
21
|
+
- [Example](#example)
|
22
|
+
- [Links](#links)
|
23
|
+
- [Related projects](#related-projects)
|
24
|
+
- [API](#api)
|
25
|
+
- [Contributing](#contributing)
|
26
|
+
|
27
|
+
## Install
|
28
|
+
|
29
|
+
TODO: once published to RubyGems.org
|
30
|
+
|
31
|
+
## Example
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require "json_p3"
|
35
|
+
require "json"
|
36
|
+
|
37
|
+
data = JSON.parse <<~JSON
|
38
|
+
{
|
39
|
+
"users": [
|
40
|
+
{
|
41
|
+
"name": "Sue",
|
42
|
+
"score": 100
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"name": "Sally",
|
46
|
+
"score": 84,
|
47
|
+
"admin": false
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"name": "John",
|
51
|
+
"score": 86,
|
52
|
+
"admin": true
|
53
|
+
},
|
54
|
+
{
|
55
|
+
"name": "Jane",
|
56
|
+
"score": 55
|
57
|
+
}
|
58
|
+
],
|
59
|
+
"moderator": "John"
|
60
|
+
}
|
61
|
+
JSON
|
62
|
+
|
63
|
+
JSONP3.find("$.users[?@.score > 85]", data).each do |node|
|
64
|
+
puts node.value
|
65
|
+
end
|
66
|
+
|
67
|
+
# {"name"=>"Sue", "score"=>100}
|
68
|
+
# {"name"=>"John", "score"=>86, "admin"=>true}
|
69
|
+
```
|
70
|
+
|
71
|
+
Or, reading JSON data from a file:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
require "json_p3"
|
75
|
+
require "json"
|
76
|
+
|
77
|
+
data = JSON.load_file("/path/to/some.json")
|
78
|
+
|
79
|
+
JSONP3.find("$.some.query", data).each do |node|
|
80
|
+
puts node.value
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
You could read data from a YAML formatted file too, or any data format that can be loaded into hashes and arrays.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
require "json_p3"
|
88
|
+
require "yaml"
|
89
|
+
|
90
|
+
data = YAML.load_file("/tmp/some.yaml")
|
91
|
+
|
92
|
+
JSONP3.find("$.users[?@.score > 85]", data).each do |node|
|
93
|
+
puts node.value
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
## Links
|
98
|
+
|
99
|
+
- Change log: https://github.com/jg-rp/ruby-json-p3/blob/main/CHANGELOG.md
|
100
|
+
- TODO: RubyGems
|
101
|
+
- Source code: https://github.com/jg-rp/ruby-json-p3
|
102
|
+
- Issue tracker: https://github.com/jg-rp/ruby-json-p3/issues
|
103
|
+
|
104
|
+
## Related projects
|
105
|
+
|
106
|
+
- [Python JSONPath RFC 9535](https://github.com/jg-rp/python-jsonpath-rfc9535) - A Python implementation of JSONPath that follows RFC 9535 strictly.
|
107
|
+
- [Python JSONPath](https://github.com/jg-rp/python-jsonpath) - Another Python package implementing JSONPath, but with additional features and customization options.
|
108
|
+
- [JSON P3](https://github.com/jg-rp/json-p3) - RFC 9535 implemented in TypeScript.
|
109
|
+
|
110
|
+
## API
|
111
|
+
|
112
|
+
### find
|
113
|
+
|
114
|
+
`find(query, value) -> Array[JSONPathNode]`
|
115
|
+
|
116
|
+
Apply JSONPath expression _query_ to JSON-like data _value_. An array of JSONPathNode instance is returned, one node for each value matched by _query_. The returned array will be empty if there were no matches.
|
117
|
+
|
118
|
+
Each `JSONPathNode` has:
|
119
|
+
|
120
|
+
- a `value` attribute, which is the JSON-like value associated with the node.
|
121
|
+
- a `location` attribute, which is a nested array of hash/object names and array indices that were required to reach the node's value in the target JSON document.
|
122
|
+
- a `path()` method, which returns the normalized path to the node in the target JSON document.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
require "json_p3"
|
126
|
+
require "json"
|
127
|
+
|
128
|
+
data = JSON.parse <<~JSON
|
129
|
+
{
|
130
|
+
"users": [
|
131
|
+
{
|
132
|
+
"name": "Sue",
|
133
|
+
"score": 100
|
134
|
+
},
|
135
|
+
{
|
136
|
+
"name": "Sally",
|
137
|
+
"score": 84,
|
138
|
+
"admin": false
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"name": "John",
|
142
|
+
"score": 86,
|
143
|
+
"admin": true
|
144
|
+
},
|
145
|
+
{
|
146
|
+
"name": "Jane",
|
147
|
+
"score": 55
|
148
|
+
}
|
149
|
+
],
|
150
|
+
"moderator": "John"
|
151
|
+
}
|
152
|
+
JSON
|
153
|
+
|
154
|
+
JSONP3.find("$.users[?@.score > 85]", data).each do |node|
|
155
|
+
puts "#{node.value} at #{node.path}"
|
156
|
+
end
|
157
|
+
|
158
|
+
# {"name"=>"Sue", "score"=>100} at $['users'][0]
|
159
|
+
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
|
160
|
+
```
|
161
|
+
|
162
|
+
### compile
|
163
|
+
|
164
|
+
`compile(query) -> JSONPath`
|
165
|
+
|
166
|
+
Prepare a JSONPath expression for repeated application to different JSON-like data. An instance of `JSONPath` has a `find(data)` method, which behaves similarly to the module-level `find(query, data)` method.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
require "json_p3"
|
170
|
+
require "json"
|
171
|
+
|
172
|
+
data = JSON.parse <<~JSON
|
173
|
+
{
|
174
|
+
"users": [
|
175
|
+
{
|
176
|
+
"name": "Sue",
|
177
|
+
"score": 100
|
178
|
+
},
|
179
|
+
{
|
180
|
+
"name": "Sally",
|
181
|
+
"score": 84,
|
182
|
+
"admin": false
|
183
|
+
},
|
184
|
+
{
|
185
|
+
"name": "John",
|
186
|
+
"score": 86,
|
187
|
+
"admin": true
|
188
|
+
},
|
189
|
+
{
|
190
|
+
"name": "Jane",
|
191
|
+
"score": 55
|
192
|
+
}
|
193
|
+
],
|
194
|
+
"moderator": "John"
|
195
|
+
}
|
196
|
+
JSON
|
197
|
+
|
198
|
+
path = JSONP3.compile("$.users[?@.score > 85]")
|
199
|
+
|
200
|
+
path.find(data).each do |node|
|
201
|
+
puts "#{node.value} at #{node.path}"
|
202
|
+
end
|
203
|
+
|
204
|
+
# {"name"=>"Sue", "score"=>100} at $['users'][0]
|
205
|
+
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
|
206
|
+
```
|
207
|
+
|
208
|
+
### JSONPathEnvironment
|
209
|
+
|
210
|
+
The `find` and `compile` methods described above are convenience methods equivalent to
|
211
|
+
|
212
|
+
```
|
213
|
+
JSONP3::DEFAULT_ENVIRONMENT.find(query, data)
|
214
|
+
```
|
215
|
+
|
216
|
+
and
|
217
|
+
|
218
|
+
```
|
219
|
+
JSONP3::DEFAULT_ENVIRONMENT.compile(query)
|
220
|
+
```
|
221
|
+
|
222
|
+
You could create your own environment like this:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
require "json_p3"
|
226
|
+
|
227
|
+
jsonpath = JSONP3::JSONPathEnvironment.new
|
228
|
+
nodes = jsonpath.find("$.*", { "a" => "b", "c" => "d" })
|
229
|
+
pp nodes.map(&:value) # ["b", "d"]
|
230
|
+
```
|
231
|
+
|
232
|
+
To configure an environment with custom filter functions or non-standard selectors, inherit from `JSONPathEnvironment` and override some of its constants or `#setup_function_extensions` method.
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class MyJSONPathEnvironment < JSONP3::JSONPathEnvironment
|
236
|
+
# The maximum integer allowed when selecting array items by index.
|
237
|
+
MAX_INT_INDEX = (2**53) - 1
|
238
|
+
|
239
|
+
# The minimum integer allowed when selecting array items by index.
|
240
|
+
MIN_INT_INDEX = -(2**53) + 1
|
241
|
+
|
242
|
+
# The maximum number of arrays and hashes the recursive descent segment will
|
243
|
+
# traverse before raising a {JSONPathRecursionError}.
|
244
|
+
MAX_RECURSION_DEPTH = 100
|
245
|
+
|
246
|
+
# One of the available implementations of the _name selector_.
|
247
|
+
#
|
248
|
+
# - {NameSelector} (the default) will select values from hashes using string keys.
|
249
|
+
# - {SymbolNameSelector} will select values from hashes using string or symbol keys.
|
250
|
+
#
|
251
|
+
# Implement your own name selector by inheriting from {NameSelector} and overriding
|
252
|
+
# `#resolve`.
|
253
|
+
NAME_SELECTOR = NameSelector
|
254
|
+
|
255
|
+
# An implementation of the _index selector_. The default implementation will
|
256
|
+
# select value from arrays only. Implement your own by inheriting from
|
257
|
+
# {IndexSelector} and overriding `#resolve`.
|
258
|
+
INDEX_SELECTOR = IndexSelector
|
259
|
+
|
260
|
+
# Override this function to configure JSONPath function extensions.
|
261
|
+
# By default, only the standard functions described in RFC 9535 are enabled.
|
262
|
+
def setup_function_extensions
|
263
|
+
@function_extensions["length"] = Length.new
|
264
|
+
@function_extensions["count"] = Count.new
|
265
|
+
@function_extensions["value"] = Value.new
|
266
|
+
@function_extensions["match"] = Match.new
|
267
|
+
@function_extensions["search"] = Search.new
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
271
|
+
### JSONPathError
|
272
|
+
|
273
|
+
`JSONPathError` is the base class for all JSONPath exceptions. The following classes inherit from `JSONPathError` and will only occur when parsing a JSONPath expression, not when applying a path to some data.
|
274
|
+
|
275
|
+
- `JSONPathSyntaxError`
|
276
|
+
- `JSONPathTypeError`
|
277
|
+
- `JSONPathNameError`
|
278
|
+
|
279
|
+
`JSONPathError` implements `#detailed_message`. With recent versions of Ruby you should get useful error messages.
|
280
|
+
|
281
|
+
```
|
282
|
+
JSONP3::JSONPathSyntaxError: unexpected trailing whitespace
|
283
|
+
-> '$.foo ' 1:5
|
284
|
+
|
|
285
|
+
1 | $.foo
|
286
|
+
| ^ unexpected trailing whitespace
|
287
|
+
```
|
288
|
+
|
289
|
+
## Contributing
|
290
|
+
|
291
|
+
Your contributions and questions are always welcome. Feel free to ask questions, report bugs or request features on the [issue tracker](https://github.com/jg-rp/ruby-json-p3/issues) or on [Github Discussions](https://github.com/jg-rp/ruby-json-p3/discussions). Pull requests are welcome too.
|
292
|
+
|
293
|
+
### Development
|
294
|
+
|
295
|
+
The [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite) is included as a git [submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules). Clone the Ruby JSONPath RFC 9535 git repository and initialize the CTS submodule.
|
296
|
+
|
297
|
+
```shell
|
298
|
+
$ git clone git@github.com:jg-rp/ruby-json-p3.git
|
299
|
+
$ cd ruby-json-p3.git
|
300
|
+
$ git submodule update --init
|
301
|
+
```
|
302
|
+
|
303
|
+
We use [Bundler](https://bundler.io/) and [Rake](https://ruby.github.io/rake/). Install development dependencies with
|
304
|
+
|
305
|
+
```
|
306
|
+
bundle install
|
307
|
+
```
|
308
|
+
|
309
|
+
Run tests with
|
310
|
+
|
311
|
+
```
|
312
|
+
bundle exec rake test
|
313
|
+
```
|
314
|
+
|
315
|
+
Lint with
|
316
|
+
|
317
|
+
```
|
318
|
+
bundle exec rubocop
|
319
|
+
```
|
320
|
+
|
321
|
+
And type check with
|
322
|
+
|
323
|
+
```
|
324
|
+
bundle exec steep
|
325
|
+
```
|
326
|
+
|
327
|
+
Run one of the benchmarks with
|
328
|
+
|
329
|
+
```
|
330
|
+
bundle exec ruby performance/benchmark_ips.rb
|
331
|
+
```
|
332
|
+
|
333
|
+
### Profiling
|
334
|
+
|
335
|
+
#### CPU profile
|
336
|
+
|
337
|
+
Dump profile data with `bundle exec ruby performance/profile.rb`, then generate an HTML flame graph with
|
338
|
+
|
339
|
+
```
|
340
|
+
bundle exec stackprof --d3-flamegraph .stackprof-cpu-just-compile.dump > flamegraph-cpu-just-compile.html
|
341
|
+
```
|
342
|
+
|
343
|
+
#### Memory profile
|
344
|
+
|
345
|
+
Print memory usage to the terminal.
|
346
|
+
|
347
|
+
```
|
348
|
+
bundle exec ruby performance/memory_profile.rb
|
349
|
+
```
|
350
|
+
|
351
|
+
### TruffleRuby
|
352
|
+
|
353
|
+
On macOS Sonoma using MacPorts and `rbenv`, `LIBYAML_PREFIX=/opt/local/lib` is needed to install TruffleRuby and when executing any `bundle` command.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "minitest/test_task"
|
5
|
+
|
6
|
+
Minitest::TestTask.create
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new do |task|
|
11
|
+
task.requires << "rubocop-minitest"
|
12
|
+
task.requires << "rubocop-rake"
|
13
|
+
task.requires << "rubocop-performance"
|
14
|
+
end
|
15
|
+
|
16
|
+
require "steep/rake_task"
|
17
|
+
|
18
|
+
Steep::RakeTask.new do |t|
|
19
|
+
t.check.severity_level = :error
|
20
|
+
t.watch.verbose
|
21
|
+
end
|
22
|
+
|
23
|
+
task default: %i[test rubocop steep]
|
data/Steepfile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
D = Steep::Diagnostic
|
4
|
+
|
5
|
+
target :lib do
|
6
|
+
signature "sig"
|
7
|
+
|
8
|
+
check "lib" # Directory name
|
9
|
+
# check "Gemfile" # File name
|
10
|
+
# check "app/models/**/*.rb" # Glob
|
11
|
+
# ignore "lib/templates/*.rb"
|
12
|
+
|
13
|
+
# library "pathname" # Standard libraries
|
14
|
+
# library "minitest"
|
15
|
+
# library "minitest/autorun"
|
16
|
+
|
17
|
+
library "json"
|
18
|
+
library "strscan"
|
19
|
+
|
20
|
+
# configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
|
21
|
+
# configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
|
22
|
+
# configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
|
23
|
+
# configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
|
24
|
+
# configure_code_diagnostics do |hash| # You can setup everything yourself
|
25
|
+
# hash[D::Ruby::NoMethod] = :information
|
26
|
+
# end
|
27
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONP3
|
4
|
+
# A least recently used cache relying on Ruby hash insertion order.
|
5
|
+
class LRUCache
|
6
|
+
attr_reader :max_size
|
7
|
+
|
8
|
+
def initialize(max_size = 128)
|
9
|
+
@data = {}
|
10
|
+
@max_size = max_size
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return the cached value or nil if _key_ does not exist.
|
14
|
+
def [](key)
|
15
|
+
val = @data[key]
|
16
|
+
return nil if val.nil?
|
17
|
+
|
18
|
+
@data.delete(key)
|
19
|
+
@data[key] = val
|
20
|
+
val
|
21
|
+
end
|
22
|
+
|
23
|
+
def []=(key, value)
|
24
|
+
if @data.key?(key)
|
25
|
+
@data.delete(key)
|
26
|
+
elsif @data.length >= @max_size
|
27
|
+
@data.delete(@data.first[0])
|
28
|
+
end
|
29
|
+
@data[key] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def length
|
33
|
+
@data.length
|
34
|
+
end
|
35
|
+
|
36
|
+
def keys
|
37
|
+
@data.keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lexer"
|
4
|
+
require_relative "parser"
|
5
|
+
require_relative "path"
|
6
|
+
require_relative "function_extensions/length"
|
7
|
+
require_relative "function_extensions/value"
|
8
|
+
require_relative "function_extensions/count"
|
9
|
+
require_relative "function_extensions/match"
|
10
|
+
require_relative "function_extensions/search"
|
11
|
+
|
12
|
+
module JSONP3
|
13
|
+
# JSONPath configuration
|
14
|
+
#
|
15
|
+
# Configure an environment by inheriting from `JSONPathEnvironment` and setting one
|
16
|
+
# or more constants and/or overriding {setup_function_extensions}.
|
17
|
+
class JSONPathEnvironment
|
18
|
+
# The maximum integer allowed when selecting array items by index.
|
19
|
+
MAX_INT_INDEX = (2**53) - 1
|
20
|
+
|
21
|
+
# The minimum integer allowed when selecting array items by index.
|
22
|
+
MIN_INT_INDEX = -(2**53) + 1
|
23
|
+
|
24
|
+
# The maximum number of arrays and hashes the recursive descent segment will
|
25
|
+
# traverse before raising a {JSONPathRecursionError}.
|
26
|
+
MAX_RECURSION_DEPTH = 100
|
27
|
+
|
28
|
+
# One of the available implementations of the _name selector_.
|
29
|
+
#
|
30
|
+
# - {NameSelector} (the default) will select values from hashes using string keys.
|
31
|
+
# - {SymbolNameSelector} will select values from hashes using string or symbol keys.
|
32
|
+
#
|
33
|
+
# Implement your own name selector by inheriting from {NameSelector} and overriding
|
34
|
+
# `#resolve`.
|
35
|
+
NAME_SELECTOR = NameSelector
|
36
|
+
|
37
|
+
# An implementation of the _index selector_. The default implementation will
|
38
|
+
# select value from arrays only. Implement your own by inheriting from
|
39
|
+
# {IndexSelector} and overriding `#resolve`.
|
40
|
+
INDEX_SELECTOR = IndexSelector
|
41
|
+
|
42
|
+
attr_accessor :function_extensions
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@parser = Parser.new(self)
|
46
|
+
@function_extensions = {}
|
47
|
+
setup_function_extensions
|
48
|
+
end
|
49
|
+
|
50
|
+
# Prepare JSONPath expression _query_ for repeated application.
|
51
|
+
# @param query [String]
|
52
|
+
# @return [JSONPath]
|
53
|
+
def compile(query)
|
54
|
+
tokens = JSONP3.tokenize(query)
|
55
|
+
JSONPath.new(self, @parser.parse(tokens))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Apply JSONPath expression _query_ to _value_.
|
59
|
+
# @param query [String] the JSONPath expression
|
60
|
+
# @param value [JSON-like data] the target JSON "document"
|
61
|
+
# @return [Array<JSONPath>]
|
62
|
+
def find(query, value)
|
63
|
+
compile(query).find(value)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Override this function to configure JSONPath function extensions.
|
67
|
+
# By default, only the standard functions described in RFC 9535 are enabled.
|
68
|
+
def setup_function_extensions
|
69
|
+
@function_extensions["length"] = Length.new
|
70
|
+
@function_extensions["count"] = Count.new
|
71
|
+
@function_extensions["value"] = Value.new
|
72
|
+
@function_extensions["match"] = Match.new
|
73
|
+
@function_extensions["search"] = Search.new
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONP3
|
4
|
+
# An exception raised when a JSONPathEnvironment is misconfigured.
|
5
|
+
class JSONPathEnvironmentError < StandardError; end
|
6
|
+
|
7
|
+
# Base class for JSONPath exceptions that happen when parsing or evaluating a query.
|
8
|
+
class JSONPathError < StandardError
|
9
|
+
FULL_MESSAGE = ((RUBY_VERSION.split(".")&.map(&:to_i) <=> [3, 2, 0]) || -1) < 1
|
10
|
+
|
11
|
+
def initialize(msg, token)
|
12
|
+
super(msg)
|
13
|
+
@token = token
|
14
|
+
end
|
15
|
+
|
16
|
+
def detailed_message(highlight: true, **_kwargs) # rubocop:disable Metrics/AbcSize
|
17
|
+
if @token.query.strip.empty?
|
18
|
+
"empty query"
|
19
|
+
else
|
20
|
+
lines = @token.query[...@token.start]&.lines or [""] # pleasing the type checker
|
21
|
+
lineno = lines.length
|
22
|
+
col = lines[-1].length
|
23
|
+
pad = " " * lineno.to_s.length
|
24
|
+
pointer = (" " * col) + ("^" * [@token.value.length, 1].max)
|
25
|
+
<<~ENDOFMESSAGE.strip
|
26
|
+
#{self.class}: #{message}
|
27
|
+
#{pad} -> '#{@token.query}' #{lineno}:#{col}
|
28
|
+
#{pad} |
|
29
|
+
#{lineno} | #{@token.query}
|
30
|
+
#{pad} | #{pointer} #{highlight ? "\e[1m#{message}\e[0m" : message}
|
31
|
+
ENDOFMESSAGE
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def full_message(highlight: true, order: :top)
|
36
|
+
if FULL_MESSAGE
|
37
|
+
# For Ruby < 3.2.0
|
38
|
+
"#{super}\n#{detailed_message(highlight: highlight, order: order)}"
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class JSONPathSyntaxError < JSONPathError; end
|
46
|
+
class JSONPathTypeError < JSONPathError; end
|
47
|
+
class JSONPathNameError < JSONPathError; end
|
48
|
+
class JSONPathRecursionError < JSONPathError; end
|
49
|
+
end
|