json_path_rfc9535 1.0.0

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: 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: []