json_p3 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --no-private
2
+ --markup markdown
3
+ lib/**/*.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## [0.2.1] - 2024-10-24
2
+
3
+ - Rename project and gem
4
+
5
+ ## [0.2.0] - 2024-10-24
6
+
7
+ - Initial release
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