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 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