json_path_rfc9535 1.0.0

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: 341631a563a6872bcf0f04f58fdb547a5a17500f3f7fae04ba29ada044c17dbd
4
+ data.tar.gz: '0669414a8c33cd4ddfd67f2e330d72783ec862ba1a26573053a5bbf372a6b26e'
5
+ SHA512:
6
+ metadata.gz: c355da298d64c50f35f59a76bbff44b2511de1dafbf65181a078db74909433396dd86f803ecd1b821ce0bf4327264786298d7189e5c7ee37777bf35e53dacc8e
7
+ data.tar.gz: f8e4f5e8a131adb0b1d88b9a820218687db2a146fea1a4ef91e33adb461b1d299285895c5fde88554f6ac14d87b7e86235ed97734253f397e06e741a5f270b13
data/.rubocop.yml ADDED
@@ -0,0 +1,455 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Exclude:
4
+ - 'bin/**'
5
+
6
+ Gemspec/DeprecatedAttributeAssignment:
7
+ Enabled: true
8
+
9
+ Gemspec/DevelopmentDependencies:
10
+ Enabled: true
11
+
12
+ Gemspec/RequireMFA:
13
+ Enabled: false
14
+
15
+ Layout/AccessModifierIndentation:
16
+ EnforcedStyle: outdent
17
+
18
+ Layout/BeginEndAlignment:
19
+ EnforcedStyleAlignWith: begin
20
+
21
+ Layout/BlockAlignment:
22
+ EnforcedStyleAlignWith: start_of_block
23
+
24
+ Layout/CommentIndentation:
25
+ AllowForAlignment: true
26
+
27
+ Layout/EmptyLineAfterMultilineCondition:
28
+ Enabled: true
29
+
30
+ Layout/EndOfLine:
31
+ EnforcedStyle: lf
32
+
33
+ Layout/ExtraSpacing:
34
+ AllowForAlignment: true
35
+ AllowBeforeTrailingComments: true
36
+
37
+ Layout/HashAlignment:
38
+ EnforcedHashRocketStyle: table
39
+ EnforcedColonStyle: table
40
+ EnforcedLastArgumentHashStyle: ignore_implicit
41
+
42
+ Layout/LineContinuationLeadingSpace:
43
+ Enabled: true
44
+
45
+ Layout/LineContinuationSpacing:
46
+ Enabled: true
47
+
48
+ Layout/LineEndStringConcatenationIndentation:
49
+ Enabled: true
50
+
51
+ Layout/MultilineArrayLineBreaks:
52
+ Enabled: true
53
+
54
+ Layout/MultilineAssignmentLayout:
55
+ Enabled: true
56
+ EnforcedStyle: same_line
57
+
58
+ Layout/MultilineHashKeyLineBreaks:
59
+ Enabled: true
60
+
61
+ Layout/MultilineMethodArgumentLineBreaks:
62
+ Enabled: true
63
+
64
+ Layout/MultilineMethodCallIndentation:
65
+ EnforcedStyle: indented_relative_to_receiver
66
+
67
+ Layout/SingleLineBlockChain:
68
+ Enabled: true
69
+
70
+ Layout/SpaceAroundEqualsInParameterDefault:
71
+ EnforcedStyle: no_space
72
+
73
+ Layout/SpaceAroundOperators:
74
+ EnforcedStyleForExponentOperator: space
75
+
76
+ Layout/SpaceBeforeBrackets:
77
+ Enabled: true
78
+
79
+ Layout/SpaceInsideHashLiteralBraces:
80
+ EnforcedStyle: no_space
81
+
82
+ Layout/TrailingWhitespace:
83
+ AllowInHeredoc: true
84
+
85
+ Lint/AmbiguousAssignment:
86
+ Enabled: true
87
+
88
+ Lint/AmbiguousOperatorPrecedence:
89
+ Enabled: true
90
+
91
+ Lint/AmbiguousRange:
92
+ Enabled: true
93
+ RequireParenthesesForMethodChains: true
94
+
95
+ Lint/AssignmentInCondition:
96
+ AllowSafeAssignment: false
97
+
98
+ Lint/ConstantOverwrittenInRescue:
99
+ Enabled: true
100
+
101
+ Lint/DeprecatedConstants:
102
+ Enabled: true
103
+
104
+ Lint/DuplicateBranch:
105
+ Enabled: true
106
+ IgnoreLiteralBranches: true
107
+ IgnoreConstantBranches: true
108
+
109
+ Lint/DuplicateMagicComment:
110
+ Enabled: true
111
+
112
+ Lint/DuplicateRegexpCharacterClassElement:
113
+ Enabled: true
114
+
115
+ Lint/EmptyBlock:
116
+ Enabled: true
117
+
118
+ Lint/EmptyClass:
119
+ Enabled: true
120
+ AllowComments: true
121
+
122
+ Lint/EmptyInPattern:
123
+ Enabled: true
124
+
125
+ Lint/HeredocMethodCallPosition:
126
+ Enabled: true
127
+
128
+ Lint/IncompatibleIoSelectWithFiberScheduler:
129
+ Enabled: false
130
+
131
+ Lint/LambdaWithoutLiteralBlock:
132
+ Enabled: true
133
+
134
+ Lint/NoReturnInBeginEndBlocks:
135
+ Enabled: true
136
+
137
+ Lint/NonAtomicFileOperation:
138
+ Enabled: true
139
+
140
+ Lint/NumberedParameterAssignment:
141
+ Enabled: true
142
+
143
+ Lint/OrAssignmentToConstant:
144
+ Enabled: true
145
+
146
+ Lint/RedundantDirGlobSort:
147
+ Enabled: true
148
+
149
+ Lint/RedundantSplatExpansion:
150
+ AllowPercentLiteralArrayArgument: false
151
+
152
+ Lint/RefinementImportMethods:
153
+ Enabled: true
154
+
155
+ Lint/RequireRangeParentheses:
156
+ Enabled: true
157
+
158
+ Lint/RequireRelativeSelfPath:
159
+ Enabled: true
160
+
161
+ Lint/SymbolConversion:
162
+ Enabled: true
163
+
164
+ Lint/ToEnumArguments:
165
+ Enabled: true
166
+
167
+ Lint/TripleQuotes:
168
+ Enabled: true
169
+
170
+ Lint/UnexpectedBlockArity:
171
+ Enabled: true
172
+
173
+ Lint/UnmodifiedReduceAccumulator:
174
+ Enabled: true
175
+
176
+ Lint/UnusedBlockArgument:
177
+ AutoCorrect: false
178
+
179
+ Lint/UnusedMethodArgument:
180
+ AutoCorrect: false
181
+
182
+ Lint/UselessRescue:
183
+ Enabled: true
184
+
185
+ Lint/UselessRuby2Keywords:
186
+ Enabled: true
187
+
188
+ Metrics:
189
+ Enabled: false
190
+
191
+ Naming/BlockForwarding:
192
+ Enabled: true
193
+
194
+ Naming/InclusiveLanguage:
195
+ Enabled: false
196
+
197
+ Security/CompoundHash:
198
+ Enabled: true
199
+
200
+ Security/Eval:
201
+ Enabled: false
202
+
203
+ Security/IoMethods:
204
+ Enabled: true
205
+
206
+ Security/YAMLLoad:
207
+ Enabled: false
208
+
209
+ Style/AccessorGrouping:
210
+ EnforcedStyle: separated
211
+
212
+ Style/ArgumentsForwarding:
213
+ Enabled: false
214
+
215
+ Style/ArrayIntersect:
216
+ Enabled: true
217
+
218
+ Style/AutoResourceCleanup:
219
+ Enabled: true
220
+
221
+ Style/CollectionCompact:
222
+ Enabled: true
223
+
224
+ Style/CollectionMethods:
225
+ Enabled: true
226
+
227
+ Style/ComparableClamp:
228
+ Enabled: true
229
+
230
+ Style/ConcatArrayLiterals:
231
+ Enabled: true
232
+
233
+ Style/DirEmpty:
234
+ Enabled: true
235
+
236
+ Style/DocumentDynamicEvalDefinition:
237
+ Enabled: false
238
+
239
+ Style/Documentation:
240
+ Enabled: false
241
+
242
+ Style/DocumentationMethod:
243
+ Enabled: false
244
+
245
+ Style/DoubleNegation:
246
+ EnforcedStyle: forbidden
247
+
248
+ Style/EmptyHeredoc:
249
+ Enabled: true
250
+
251
+ Style/EmptyMethod:
252
+ EnforcedStyle: expanded
253
+
254
+ Style/EndlessMethod:
255
+ Enabled: true
256
+ EnforcedStyle: disallow
257
+
258
+ Style/EnvHome:
259
+ Enabled: true
260
+
261
+ Style/FetchEnvVar:
262
+ Enabled: false
263
+
264
+ Style/FileEmpty:
265
+ Enabled: true
266
+
267
+ Style/FileRead:
268
+ Enabled: true
269
+
270
+ Style/FileWrite:
271
+ Enabled: true
272
+
273
+ Style/FormatString:
274
+ EnforcedStyle: percent
275
+
276
+ Style/FrozenStringLiteralComment:
277
+ Enabled: false
278
+
279
+ Style/HashConversion:
280
+ Enabled: true
281
+
282
+ Style/HashExcept:
283
+ Enabled: true
284
+
285
+ Style/HashSyntax:
286
+ EnforcedShorthandSyntax: never
287
+
288
+ Style/IfWithBooleanLiteralBranches:
289
+ Enabled: true
290
+
291
+ Style/ImplicitRuntimeError:
292
+ Enabled: true
293
+
294
+ Style/InPatternThen:
295
+ Enabled: true
296
+
297
+ Style/IpAddresses:
298
+ Enabled: true
299
+
300
+ Style/MagicCommentFormat:
301
+ Enabled: true
302
+
303
+ Style/MapCompactWithConditionalBlock:
304
+ Enabled: true
305
+
306
+ Style/MapToHash:
307
+ Enabled: true
308
+
309
+ Style/MapToSet:
310
+ Enabled: true
311
+
312
+ Style/MethodCallWithArgsParentheses:
313
+ Enabled: true
314
+ EnforcedStyle: omit_parentheses
315
+ AllowParenthesesInMultilineCall: true
316
+ AllowParenthesesInChaining: true
317
+ AllowParenthesesInCamelCaseMethod: true
318
+
319
+ Style/MethodDefParentheses:
320
+ EnforcedStyle: require_no_parentheses_except_multiline
321
+
322
+ Style/MinMaxComparison:
323
+ Enabled: true
324
+
325
+ Style/MultilineBlockChain:
326
+ Enabled: false
327
+
328
+ Style/MultilineInPatternThen:
329
+ Enabled: true
330
+
331
+ Style/NegatedIfElseCondition:
332
+ Enabled: true
333
+
334
+ Style/NestedFileDirname:
335
+ Enabled: true
336
+
337
+ Style/NestedParenthesizedCalls:
338
+ Enabled: false
339
+
340
+ Style/NilLambda:
341
+ Enabled: true
342
+
343
+ Style/NonNilCheck:
344
+ Enabled: false
345
+
346
+ Style/NumberedParameters:
347
+ Enabled: true
348
+ EnforcedStyle: disallow
349
+
350
+ Style/NumberedParametersLimit:
351
+ Enabled: true
352
+
353
+ Style/ObjectThen:
354
+ Enabled: true
355
+
356
+ Style/OpenStructUse:
357
+ Enabled: false
358
+
359
+ Style/OperatorMethodCall:
360
+ Enabled: true
361
+
362
+ Style/OptionHash:
363
+ Enabled: true
364
+
365
+ Style/QuotedSymbols:
366
+ Enabled: true
367
+
368
+ Style/RedundantArgument:
369
+ Enabled: false
370
+
371
+ Style/RedundantConstantBase:
372
+ Enabled: false
373
+
374
+ Style/RedundantDoubleSplatHashBraces:
375
+ Enabled: true
376
+
377
+ Style/RedundantEach:
378
+ Enabled: true
379
+
380
+ Style/RedundantException:
381
+ Enabled: false
382
+
383
+ Style/RedundantHeredocDelimiterQuotes:
384
+ Enabled: true
385
+
386
+ Style/RedundantInitialize:
387
+ Enabled: true
388
+
389
+ Style/RedundantParentheses:
390
+ Enabled: false
391
+
392
+ Style/RedundantSelfAssignmentBranch:
393
+ Enabled: true
394
+
395
+ Style/RedundantStringEscape:
396
+ Enabled: true
397
+
398
+ Style/RegexpLiteral:
399
+ EnforcedStyle: percent_r
400
+
401
+ Style/ReturnNil:
402
+ Enabled: true
403
+
404
+ Style/SelectByRegexp:
405
+ Enabled: true
406
+
407
+ Style/SingleLineMethods:
408
+ AllowIfMethodIsEmpty: false
409
+
410
+ Style/StaticClass:
411
+ Enabled: true
412
+
413
+ Style/StringChars:
414
+ Enabled: true
415
+
416
+ Style/StringHashKeys:
417
+ Enabled: false
418
+
419
+ Style/SwapValues:
420
+ Enabled: true
421
+
422
+ Style/SymbolArray:
423
+ EnforcedStyle: brackets
424
+
425
+ Style/TernaryParentheses:
426
+ EnforcedStyle: require_parentheses_when_complex
427
+ AllowSafeAssignment: false
428
+
429
+ Style/TopLevelMethodDefinition:
430
+ Enabled: true
431
+
432
+ Style/TrailingCommaInArguments:
433
+ Enabled: true
434
+ EnforcedStyleForMultiline: no_comma
435
+
436
+ Style/TrailingCommaInArrayLiteral:
437
+ Enabled: true
438
+ EnforcedStyleForMultiline: no_comma
439
+
440
+ Style/TrailingCommaInBlockArgs:
441
+ Enabled: true
442
+
443
+ Style/TrailingCommaInHashLiteral:
444
+ Enabled: true
445
+ EnforcedStyleForMultiline: no_comma
446
+
447
+ Style/UnlessLogicalOperators:
448
+ EnforcedStyle: forbid_logical_operators
449
+
450
+ Style/WordArray:
451
+ EnforcedStyle: brackets
452
+
453
+ Style/YodaCondition:
454
+ Enabled: true
455
+ EnforcedStyle: forbid_for_all_comparison_operators
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ <!--[//]: # (
4
+ ## <Release number> <Date YYYY-MM-DD>
5
+ ### Breaking changes
6
+ ### Deprecations
7
+ ### New features
8
+ ### Bug fixes
9
+ )-->
10
+
11
+ ## 1.0.0 2024-09-09
12
+
13
+ First public release. Refer to [README.md](README.md) for the full documentation.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Json Path RFC 9535
2
+
3
+ A Ruby implementation of RFC 9535.
4
+
5
+ Like XPath is a query language for XML, JsonPath is a query language for JSON. This gem aims to be an implementation of RFC 9535. Unlike tha original JsonPath description (http://goessner.net/articles/JsonPath/), RFC 9535 is strictly normative, which ideally should leave open fewer doors for inconsistencies.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'json_path_rfc9535', '~> 1.0'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ $ bundle
19
+ ```
20
+
21
+ Or you can install the gem on its own:
22
+
23
+ ```bash
24
+ gem install json_path_rfc9535
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Parse the Json into a `JsonPath::Doc` instance...
30
+
31
+ ```ruby
32
+ doc = JsonPath::Doc(<<~JSON)
33
+ {
34
+ "store": {
35
+ "book": [
36
+ {
37
+ "category": "reference",
38
+ "author": "Nigel Rees",
39
+ "title": "Sayings of the Century",
40
+ "price": 8.95
41
+ },
42
+ {
43
+ "category": "fiction",
44
+ "author": "Evelyn Waugh",
45
+ "title": "Sword of Honour",
46
+ "price": 12.99
47
+ },
48
+ {
49
+ "category": "fiction",
50
+ "author": "Herman Melville",
51
+ "title": "Moby Dick",
52
+ "isbn": "0-553-21311-3",
53
+ "price": 8.99
54
+ },
55
+ {
56
+ "category": "fiction",
57
+ "author": "J. R. R. Tolkien",
58
+ "title": "The Lord of the Rings",
59
+ "isbn": "0-395-19395-8",
60
+ "price": 22.99
61
+ }
62
+ ],
63
+ "bicycle": {
64
+ "color": "red",
65
+ "price": 399
66
+ }
67
+ }
68
+ }
69
+ JSON
70
+ ```
71
+
72
+ ... and then query it.
73
+
74
+ ```ruby
75
+ doc.query('$.store.bicycle.color')
76
+ ```
77
+
78
+ The returned object has methods to retrieve the values or the paths of all the retrieved nodes:
79
+
80
+ ```ruby
81
+ doc.query('$.store.book.*.category').values
82
+ # => ["reference", "fiction", "fiction", "fiction"]
83
+ doc.query('$.store.book.*.category').paths
84
+ # => ["$['store']['book'][0]['category']", "$['store']['book'][1]['category']", "$['store']['book'][2]['category']", "$['store']['book'][3]['category']"]
85
+ ```
86
+
87
+ You can also query it further:
88
+
89
+ ```ruby
90
+ results = doc.query('$.store.book[?(@.price > 10)]')
91
+ results.paths
92
+ # => ["$['store']['book'][1]", "$['store']['book'][3]"]
93
+ results.query('$.author').values
94
+ # => ["Evelyn Waugh", "J. R. R. Tolkien"]
95
+ ```
96
+
97
+ This gem implements most of RFC 9535, with the exception of [function extensions](https://datatracker.ietf.org/doc/html/rfc9535#name-function-extensions) and the related [type system](https://datatracker.ietf.org/doc/html/rfc9535#name-type-system-for-function-ex). It also relies on the underlying Ruby interpreter for string evaluation, meaning that characters don't need to be double-escaped.
98
+
99
+ ## Plans for future development
100
+
101
+ - Function extensions
102
+ - Function extensions type system
103
+
104
+ ## Version numbers
105
+
106
+ Json Path RFC 9535 loosely follows [Semantic Versioning](https://semver.org/), with a hard guarantee that breaking changes to the public API will always coincide with an increase to the `MAJOR` number.
107
+
108
+ Version numbers are in three parts: `MAJOR.MINOR.PATCH`.
109
+
110
+ - Breaking changes to the public API increment the `MAJOR`. There may also be changes that would otherwise increase the `MINOR` or the `PATCH`.
111
+ - Additions, deprecations, and "big" non breaking changes to the public API increment the `MINOR`. There may also be changes that would otherwise increase the `PATCH`.
112
+ - Bug fixes and "small" non breaking changes to the public API increment the `PATCH`.
113
+
114
+ Notice that any feature deprecated by a minor release can be expected to be removed by the next major release.
115
+
116
+ ## Changelog
117
+
118
+ Full list of changes in [CHANGELOG.md](CHANGELOG.md)
119
+
120
+ ## Contributing
121
+
122
+ Bug reports and pull requests are welcome on GitHub at https://github.com/moku-io/json_path_rfc9535.
123
+
124
+ ## License
125
+
126
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+ require_relative 'nodes'
3
+ require_relative 'path'
4
+ require_relative 'node_list'
5
+
6
+ module JsonPath
7
+ class Doc
8
+ attr_reader :root_node
9
+
10
+ def initialize json_string, parse_json: json_string.is_a?(String)
11
+ json = (parse_json ? JSON.parse(json_string) : json_string)
12
+ @root_node = Nodes.parse '$', json
13
+ end
14
+
15
+ def query json_path
16
+ json_path = Path.new json_path
17
+
18
+ json_path
19
+ .apply(root_node)
20
+ .then { NodeList.new _1 }
21
+ end
22
+
23
+ def value
24
+ root_node.value
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,4 @@
1
+ module JsonPath
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'error'
2
+
3
+ module JsonPath
4
+ class MultipleValuesReturnedBySingularQuery < Error
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'path'
2
+
3
+ module JsonPath
4
+ class NodeList
5
+ attr_reader :nodes
6
+
7
+ def initialize nodes
8
+ @nodes = nodes
9
+ end
10
+
11
+ def query json_path
12
+ json_path = Path.new json_path
13
+
14
+ nodes
15
+ .flat_map { json_path.apply _1 }
16
+ .then { self.class.new _1 }
17
+ end
18
+
19
+ def values
20
+ nodes.map(&:value)
21
+ end
22
+
23
+ def paths
24
+ nodes.map(&:path)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class Array < Base
4
+ alias elements children
5
+
6
+ def initialize path, values
7
+ super path
8
+ @children = values
9
+ .map.with_index do |value, i|
10
+ Nodes.parse "#{path}[#{i}]", value
11
+ end
12
+ end
13
+
14
+ def value
15
+ elements.map(&:value)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class Base
4
+ attr_reader :path
5
+ attr_reader :children
6
+
7
+ def initialize path
8
+ @path = path
9
+ @children = []
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class False < Base
4
+ def value
5
+ false
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class Null < Base
4
+ def value
5
+ nil
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class Number < Base
4
+ attr_reader :value
5
+
6
+ def initialize path, value
7
+ super path
8
+ @value = value
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class Object < Base
4
+ attr_reader :hash
5
+
6
+ def initialize path, hash
7
+ super path
8
+ @hash = hash.to_h do |key, value|
9
+ [key, Nodes.parse("#{path}['#{key}']", value)]
10
+ end
11
+ @children = self.hash.values
12
+ end
13
+
14
+ def value
15
+ hash.transform_values(&:value)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class String < Base
4
+ attr_reader :value
5
+
6
+ def initialize path, value
7
+ super path
8
+ @value = value
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module JsonPath
2
+ module Nodes
3
+ class True < Base
4
+ def value
5
+ true
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'nodes/base'
2
+ Dir["#{__dir__}/nodes/**"].each { |filename| require_relative filename }
3
+
4
+ module JsonPath
5
+ module Nodes
6
+ def self.parse path, value
7
+ case value
8
+ when nil
9
+ Null.new path
10
+ when true
11
+ True.new path
12
+ when false
13
+ False.new path
14
+ when ::String
15
+ String.new path, value
16
+ when Numeric
17
+ Number.new path, value
18
+ when ::Array
19
+ Array.new path, value
20
+ when Hash
21
+ Object.new path, value
22
+ else
23
+ raise UnrecognizedNode, "JSON value expected, #{value.class} found"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,75 @@
1
+ require 'parslet'
2
+
3
+ module JsonPath
4
+ module Parser
5
+ class Core < Parslet::Parser
6
+ # Never matches
7
+ rule(:none) { match('').absent? }
8
+ # Always matches, but doesn't consume any input
9
+ rule(:empty) { str('') }
10
+
11
+ def many parser
12
+ parser.repeat
13
+ end
14
+
15
+ def some parser
16
+ parser.repeat 1
17
+ end
18
+
19
+ rule(:digit) { match('[0-9]') }
20
+ rule(:digits) { digit.repeat(1) }
21
+ rule(:double_quoted_string_char) { match('[^\\\\"]') | str('\\\\') | str('\\"') }
22
+ rule(:single_quoted_string_char) { match("[^\\\\']") | str('\\\\') | str("\\'") }
23
+ rule(:whitespace) { match('[\s]').repeat }
24
+
25
+ rule(:int) { str('-').maybe >> digits }
26
+ rule :num do
27
+ int >>
28
+ (str('.') >> digits).maybe >>
29
+ (str('e') >> (str('-') | str('+')).maybe >> digits).maybe
30
+ end
31
+
32
+ rule(:double_quoted_string) { token(str('"') >> many(double_quoted_string_char).as(:string) >> str('"')) }
33
+ rule(:single_quoted_string) { token(str("'") >> many(single_quoted_string_char).as(:string) >> str("'")) }
34
+
35
+ rule(:true_constant) { symbol('true') }
36
+ rule(:false_constant) { symbol('false') }
37
+ rule(:integer) { token(int) }
38
+ rule(:number) { token(num) }
39
+ rule(:string) { double_quoted_string | single_quoted_string }
40
+
41
+ def symbol string
42
+ token(str(string))
43
+ end
44
+
45
+ def many_separated parser, separator_parser
46
+ some_separated(parser, separator_parser).maybe
47
+ end
48
+
49
+ def some_separated parser, separator_parser
50
+ parser >> many(separator_parser >> parser)
51
+ end
52
+
53
+ def any_unless parser
54
+ parser.absent? >> any
55
+ end
56
+
57
+ def any_until parser, prefix_name=nil
58
+ prefix_parser = some(any_unless(parser))
59
+ prefix_parser = prefix_parser.as(prefix_name) if prefix_name.present?
60
+ prefix_parser
61
+ end
62
+
63
+ protected
64
+
65
+ # Takes a block which needs to be a predicate over strings
66
+ def predicate parser
67
+ parser.capture(:input) >> dynamic { |_, c| yield(c.captures[:input].to_s) ? empty : none }
68
+ end
69
+
70
+ def token parser
71
+ whitespace.maybe >> parser >> whitespace
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'core'
2
+
3
+ module JsonPath
4
+ module Parser
5
+ class RawParser < Core
6
+ rule(:jsonpath_query) { root_identifier >> many(segment).as(:segments) }
7
+ root :jsonpath_query
8
+
9
+ rule(:root_identifier) { str('$') }
10
+ rule(:current_node_identifier) { str('@') }
11
+
12
+ # Selectors
13
+ rule(:selector) { name_selector | wildcard_selector | slice_selector | index_selector | filter_selector }
14
+ rule(:name_selector) { string.as(:name) }
15
+ rule(:wildcard_selector) { str('*').as(:wildcard) }
16
+ rule(:index_selector) { int.as(:index) }
17
+ rule :slice_selector do
18
+ integer.maybe.as(:start) >>
19
+ str(':') >> whitespace >>
20
+ integer.maybe.as(:end) >>
21
+ (str(':') >> whitespace >> integer.maybe.as(:step)).maybe
22
+ end
23
+ rule(:filter_selector) { str('?') >> whitespace >> logical_expr.as(:filter) }
24
+
25
+ # Logical expressions
26
+ rule(:logical_expr) { logical_or_expr }
27
+ rule(:logical_or_expr) { some_separated(logical_and_expr.as(:logical_or_operands), symbol('||')) }
28
+ rule(:logical_and_expr) { some_separated(basic_expr.as(:logical_and_operands), symbol('&&')) }
29
+ rule(:basic_expr) { paren_expr | comparison_expr | test_expr }
30
+ rule(:logical_not_op) { symbol('!') }
31
+ rule :paren_expr do
32
+ logical_not_op.maybe.as(:negation) >> symbol('(') >> logical_expr.as(:parenthesized_expr) >> symbol(')')
33
+ end
34
+ rule(:test_expr) { logical_not_op.maybe.as(:negation) >> (filter_query | function_expr).as(:test_expr) }
35
+ rule(:filter_query) { (rel_query | jsonpath_query).as(:filter_query) }
36
+ rule(:rel_query) { current_node_identifier >> many(segment).as(:relative_segments) }
37
+ rule :comparison_expr do
38
+ comparable.as(:comparable1) >> comparison_op.as(:comparison_op) >> comparable.as(:comparable2)
39
+ end
40
+ rule(:comparison_op) { symbol('==') | symbol('!=') | symbol('<=') | symbol('>=') | symbol('<') | symbol('>') }
41
+ rule(:comparable) { literal | singular_query | function_expr }
42
+ rule :literal do
43
+ number.as(:literal_number) |
44
+ string.as(:literal_string) |
45
+ symbol('true').as(:literal_true) |
46
+ symbol('false').as(:literal_false) |
47
+ symbol('null').as(:literal_null)
48
+ end
49
+ rule(:singular_query) { (rel_singular_query | abs_singular_query).as(:singular_query) }
50
+ rule :rel_singular_query do
51
+ current_node_identifier >> many(singular_query_segment).as(:relative_segments)
52
+ end
53
+ rule :abs_singular_query do
54
+ root_identifier >> many(singular_query_segment).as(:segments)
55
+ end
56
+ rule(:singular_query_segment) { name_segment | index_segment }
57
+ rule(:name_segment) { ((str('[') >> name_selector >> str(']')) | (str('.') >> member_name_shorthand)).as(:child) }
58
+ rule(:index_segment) { (str('[') >> index_selector >> str(']')).as(:child) }
59
+ rule(:function_expr) { none }
60
+
61
+ # Segments
62
+ rule(:segment) { child_segment | descendant_segment }
63
+ rule :child_segment do
64
+ (bracketed_selection | (str('.') >> (wildcard_selector | member_name_shorthand))).as :child
65
+ end
66
+ rule(:bracketed_selection) { str('[') >> some_separated(selector, symbol(',')).as(:bracketed) >> str(']') }
67
+ rule(:member_name_shorthand) { (match('[a-zA-Z_]') >> many(match('[a-zA-Z_0-9]'))).as(:name) }
68
+ rule :descendant_segment do
69
+ (str('..') >> (bracketed_selection | wildcard_selector | member_name_shorthand)).as :descendant
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,219 @@
1
+ require 'parslet'
2
+ require_relative '../multiple_values_returned_by_singular_query'
3
+
4
+ module JsonPath
5
+ module Parser
6
+ class Transformer < Parslet::Transform
7
+ EMPTY = ::Object.new
8
+
9
+ rule segments: sequence(:segments) do
10
+ proc do |root|
11
+ segments.reduce [root] do |nodes, segment|
12
+ segment.call nodes, root
13
+ end
14
+ end
15
+ end
16
+
17
+ rule(child: simple(:segment)) { segment }
18
+
19
+ rule descendant: simple(:segment) do
20
+ proc do |nodes, root|
21
+ recursive_application = proc do |node|
22
+ segment.call([node], root) + node.children.flat_map(&recursive_application)
23
+ end
24
+
25
+ nodes.flat_map(&recursive_application)
26
+ end
27
+ end
28
+
29
+ rule(bracketed: simple(:segment)) { segment }
30
+
31
+ rule bracketed: sequence(:segments) do
32
+ proc do |nodes, root|
33
+ segments.flat_map { _1.call nodes, root }
34
+ end
35
+ end
36
+
37
+ rule name: simple(:name) do
38
+ proc do |nodes|
39
+ nodes.filter_map do |node|
40
+ node.hash[name.to_s] if node.is_a? Nodes::Object
41
+ end
42
+ end
43
+ end
44
+
45
+ rule wildcard: simple(:x) do
46
+ proc do |nodes|
47
+ nodes.flat_map(&:children)
48
+ end
49
+ end
50
+
51
+ rule index: simple(:index) do
52
+ proc do |nodes|
53
+ nodes.filter_map do |node|
54
+ node.elements[Integer(index)] if node.is_a? Nodes::Array
55
+ end
56
+ end
57
+ end
58
+
59
+ rule start: simple(:slice_start),
60
+ end: simple(:slice_end),
61
+ step: simple(:slice_step) do
62
+ proc do |nodes|
63
+ slice_step = Integer(self.slice_step) || 1
64
+
65
+ next [] if slice_step.zero?
66
+
67
+ nodes.flat_map do |node|
68
+ next [] unless node.is_a? Nodes::Array
69
+
70
+ len = node.elements.size
71
+
72
+ slice_start, slice_end = if slice_step.positive?
73
+ [
74
+ Integer(self.slice_start) || 0,
75
+ Integer(self.slice_end) || len
76
+ ]
77
+ else
78
+ [
79
+ Integer(self.slice_start) || (len - 1),
80
+ Integer(self.slice_end) || (-len - 1)
81
+ ]
82
+ end
83
+
84
+ slice_start += len if slice_start.negative?
85
+ slice_end += len if slice_end.negative?
86
+
87
+ lower, upper = if slice_step.positive?
88
+ [
89
+ [[slice_start, 0].max, len].min,
90
+ [[slice_end, 0].max, len].min
91
+ ]
92
+ else
93
+ [
94
+ [[slice_end, -1].max, len - 1].min,
95
+ [[slice_start, -1].max, len - 1].min
96
+ ]
97
+ end
98
+
99
+ # This is horrible, but it's also the easiest way to implement the semantics from the RFC
100
+ [].tap do |result|
101
+ if slice_step.positive?
102
+ i = lower
103
+ while i < upper
104
+ result << node.elements[i] if (0...node.elements.size).include? i
105
+ i += slice_step
106
+ end
107
+ else
108
+ i = upper
109
+ while i > lower
110
+ result << node.elements[i] if (0...node.elements.size).include? i
111
+ i += step
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ rule filter: simple(:filter) do
120
+ proc do |nodes, root|
121
+ nodes.flat_map do |node|
122
+ case node
123
+ when Nodes::Array
124
+ node.elements
125
+ when Nodes::Object
126
+ node.hash.values
127
+ else
128
+ []
129
+ end
130
+ .filter { filter.call root, _1 }
131
+ end
132
+ end
133
+ end
134
+
135
+ rule(logical_or_operands: simple(:operand)) { operand }
136
+ rule(logical_and_operands: simple(:operand)) { operand }
137
+
138
+ rule logical_or_operands: sequence(:operands) do
139
+ proc do |*args, **kwargs, &block|
140
+ operands.any? { _1.call(*args, **kwargs, &block) }
141
+ end
142
+ end
143
+
144
+ rule logical_and_operands: sequence(:operands) do
145
+ proc do |*args, **kwargs, &block|
146
+ operands.all? { _1.call(*args, **kwargs, &block) }
147
+ end
148
+ end
149
+
150
+ rule negation: simple(:negation),
151
+ test_expr: simple(:expr) do
152
+ expr >> proc { negation ? !_1 : _1 }
153
+ end
154
+
155
+ rule filter_query: simple(:query) do
156
+ query >> proc { !_1.empty? }
157
+ end
158
+
159
+ rule negation: simple(:negation),
160
+ parenthesized_expr: simple(:expr) do
161
+ expr >> proc { negation ? !_1 : _1 }
162
+ end
163
+
164
+ rule comparable1: simple(:comp1),
165
+ comparison_op: simple(:op),
166
+ comparable2: simple(:comp2) do
167
+ proc do |*args, **kwargs, &block|
168
+ [comp1.call(*args, **kwargs, &block), comp2.call(*args, **kwargs, &block)]
169
+ end >> proc { _1.public_send op.to_s.strip.to_sym, _2 }
170
+ end
171
+
172
+ rule relative_segments: sequence(:segments) do
173
+ proc do |root, current_node|
174
+ segments.reduce [current_node] do |nodes, segment|
175
+ segment.call nodes, root
176
+ end
177
+ end
178
+ end
179
+
180
+ rule literal_number: simple(:num) do
181
+ proc { eval num }
182
+ end
183
+
184
+ rule literal_string: simple(:string) do
185
+ proc { string.to_s }
186
+ end
187
+
188
+ rule literal_true: simple(:x) do
189
+ proc { true }
190
+ end
191
+
192
+ rule literal_false: simple(:x) do
193
+ proc { false }
194
+ end
195
+
196
+ rule literal_null: simple(:x) do
197
+ proc {}
198
+ end
199
+
200
+ rule string: simple(:string) do
201
+ string
202
+ end
203
+
204
+ rule singular_query: simple(:query) do
205
+ proc do |*args, **kwargs, &block|
206
+ nodes = query.call(*args, **kwargs, &block)
207
+
208
+ if nodes.empty?
209
+ EMPTY
210
+ elsif nodes.size == 1
211
+ nodes.first.value
212
+ else
213
+ raise MultipleValuesReturnedBySingularQuery, 'Singular query must return single value'
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'parser/raw_parser'
2
+ require_relative 'parser/transformer'
3
+
4
+ module JsonPath
5
+ module Parser
6
+ RAW_PARSER = RawParser.new
7
+ TRANSFORMER = Transformer.new
8
+
9
+ def self.compile json_path_string
10
+ reporter = Parslet::ErrorReporter::Contextual.new
11
+ TRANSFORMER.apply RAW_PARSER.parse(json_path_string, reporter: reporter)
12
+ rescue Parslet::ParseFailed
13
+ reporter.deepest_cause.raise
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'parser'
2
+
3
+ module JsonPath
4
+ class Path
5
+ attr_reader :string
6
+ attr_reader :proc
7
+
8
+ def initialize json_path
9
+ if json_path.is_a? Path
10
+ @string = json_path.string
11
+ @proc = json_path.proc
12
+ else
13
+ @string = -json_path
14
+ @proc = Parser.compile json_path
15
+ end
16
+ end
17
+
18
+ def apply node
19
+ proc.call node
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module JsonPath
2
+ class UnrecognizedNode < Error
3
+ end
4
+ end
data/lib/json_path.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative 'json_path/doc'
2
+
3
+ module JsonPath
4
+ def self.Doc(...)
5
+ Doc.new(...)
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module JsonPathRfc9535
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'json_path_rfc9535/version'
2
+ require_relative 'json_path'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_path_rfc9535
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Moku S.r.l.
8
+ - Riccardo Agatea
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2024-09-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: parslet
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.0'
28
+ description: Like XPath is a query language for XML, JsonPath is a query language
29
+ for JSON. This gem aims to be an implementation of RFC 9535. Unlike tha original
30
+ JsonPath description (http://goessner.net/articles/JsonPath/), RFC 9535 is strictly
31
+ normative, which ideally should leave open fewer doors for inconsistencies.
32
+ email:
33
+ - info@moku.io
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".rubocop.yml"
39
+ - CHANGELOG.md
40
+ - README.md
41
+ - lib/json_path.rb
42
+ - lib/json_path/doc.rb
43
+ - lib/json_path/error.rb
44
+ - lib/json_path/multiple_values_returned_by_singular_query.rb
45
+ - lib/json_path/node_list.rb
46
+ - lib/json_path/nodes.rb
47
+ - lib/json_path/nodes/array.rb
48
+ - lib/json_path/nodes/base.rb
49
+ - lib/json_path/nodes/false.rb
50
+ - lib/json_path/nodes/null.rb
51
+ - lib/json_path/nodes/number.rb
52
+ - lib/json_path/nodes/object.rb
53
+ - lib/json_path/nodes/string.rb
54
+ - lib/json_path/nodes/true.rb
55
+ - lib/json_path/parser.rb
56
+ - lib/json_path/parser/core.rb
57
+ - lib/json_path/parser/raw_parser.rb
58
+ - lib/json_path/parser/transformer.rb
59
+ - lib/json_path/path.rb
60
+ - lib/json_path/unrecognized_node.rb
61
+ - lib/json_path_rfc9535.rb
62
+ - lib/json_path_rfc9535/version.rb
63
+ homepage: https://github.com/moku-io/json_path_rfc9535
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://github.com/moku-io/json_path_rfc9535
68
+ source_code_uri: https://github.com/moku-io/json_path_rfc9535
69
+ changelog_uri: https://github.com/moku-io/json_path_rfc9535/blob/master/CHANGELOG.md
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 3.0.0
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 3.5.11
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: A Ruby implementation of RFC 9535.
89
+ test_files: []