json_p3 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +14 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +7 -0
- data/LICENCE +21 -0
- data/README.md +353 -0
- data/Rakefile +23 -0
- data/Steepfile +27 -0
- data/lib/json_p3/cache.rb +40 -0
- data/lib/json_p3/environment.rb +76 -0
- data/lib/json_p3/errors.rb +49 -0
- data/lib/json_p3/filter.rb +426 -0
- data/lib/json_p3/function.rb +16 -0
- data/lib/json_p3/function_extensions/count.rb +15 -0
- data/lib/json_p3/function_extensions/length.rb +17 -0
- data/lib/json_p3/function_extensions/match.rb +62 -0
- data/lib/json_p3/function_extensions/pattern.rb +39 -0
- data/lib/json_p3/function_extensions/search.rb +44 -0
- data/lib/json_p3/function_extensions/value.rb +15 -0
- data/lib/json_p3/lexer.rb +420 -0
- data/lib/json_p3/node.rb +42 -0
- data/lib/json_p3/parser.rb +553 -0
- data/lib/json_p3/path.rb +42 -0
- data/lib/json_p3/segment.rb +102 -0
- data/lib/json_p3/selector.rb +285 -0
- data/lib/json_p3/token.rb +74 -0
- data/lib/json_p3/unescape.rb +112 -0
- data/lib/json_p3/version.rb +5 -0
- data/lib/json_p3.rb +17 -0
- data/performance/benchmark.rb +33 -0
- data/performance/benchmark_ips.rb +29 -0
- data/performance/benchmark_small_citylots.rb +18 -0
- data/performance/memory_profile.rb +19 -0
- data/performance/memory_profile_small_citylots.rb +14 -0
- data/performance/profile.rb +30 -0
- data/sig/json_p3.rbs +1058 -0
- data.tar.gz.sig +1 -0
- metadata +110 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,553 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "set"
|
5
|
+
|
6
|
+
require_relative "errors"
|
7
|
+
require_relative "filter"
|
8
|
+
require_relative "function"
|
9
|
+
require_relative "segment"
|
10
|
+
require_relative "selector"
|
11
|
+
require_relative "token"
|
12
|
+
require_relative "unescape"
|
13
|
+
|
14
|
+
module JSONP3
|
15
|
+
# Step through tokens
|
16
|
+
class Stream
|
17
|
+
def initialize(tokens)
|
18
|
+
@tokens = tokens
|
19
|
+
@index = 0
|
20
|
+
@eoi = tokens.last
|
21
|
+
end
|
22
|
+
|
23
|
+
def next
|
24
|
+
token = @tokens.fetch(@index)
|
25
|
+
@index += 1
|
26
|
+
token
|
27
|
+
rescue IndexError
|
28
|
+
@eor
|
29
|
+
end
|
30
|
+
|
31
|
+
def peek
|
32
|
+
@tokens.fetch(@index)
|
33
|
+
rescue IndexError
|
34
|
+
@eor
|
35
|
+
end
|
36
|
+
|
37
|
+
def expect(token_type)
|
38
|
+
return if peek.type == token_type
|
39
|
+
|
40
|
+
token = self.next
|
41
|
+
raise JSONPathSyntaxError.new("expected #{token_type}, found #{token.type}", token)
|
42
|
+
end
|
43
|
+
|
44
|
+
def expect_not(token_type, message)
|
45
|
+
return unless peek.type == token_type
|
46
|
+
|
47
|
+
token = self.next
|
48
|
+
raise JSONPathSyntaxError.new(message, token)
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"JSONP3::stream(head=#{peek.inspect})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Precedence
|
57
|
+
LOWEST = 1
|
58
|
+
LOGICAL_OR = 3
|
59
|
+
LOGICAL_AND = 4
|
60
|
+
RELATIONAL = 5
|
61
|
+
PREFIX = 7
|
62
|
+
end
|
63
|
+
|
64
|
+
# A JSONPath expression parser.
|
65
|
+
class Parser # rubocop:disable Metrics/ClassLength
|
66
|
+
def initialize(env)
|
67
|
+
@env = env
|
68
|
+
@name_selector = env.class::NAME_SELECTOR
|
69
|
+
@index_selector = env.class::INDEX_SELECTOR
|
70
|
+
end
|
71
|
+
|
72
|
+
# Parse an array of tokens into an abstract syntax tree.
|
73
|
+
# @param tokens [Array<Token>] tokens from the lexer.
|
74
|
+
# @return [Array<Segment>]
|
75
|
+
def parse(tokens)
|
76
|
+
stream = Stream.new(tokens)
|
77
|
+
stream.expect(Token::ROOT)
|
78
|
+
stream.next
|
79
|
+
parse_query(stream)
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def parse_query(stream) # rubocop:disable Metrics/MethodLength
|
85
|
+
segments = []
|
86
|
+
|
87
|
+
loop do
|
88
|
+
case stream.peek.type
|
89
|
+
when Token::DOUBLE_DOT
|
90
|
+
token = stream.next
|
91
|
+
selectors = parse_selectors(stream)
|
92
|
+
segments << RecursiveDescentSegment.new(@env, token, selectors)
|
93
|
+
when Token::LBRACKET, Token::NAME, Token::WILD
|
94
|
+
token = stream.peek
|
95
|
+
selectors = parse_selectors(stream)
|
96
|
+
segments << ChildSegment.new(@env, token, selectors)
|
97
|
+
else
|
98
|
+
break
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
segments
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_selectors(stream) # rubocop:disable Metrics/MethodLength
|
106
|
+
case stream.peek.type
|
107
|
+
when Token::NAME
|
108
|
+
token = stream.next
|
109
|
+
[@name_selector.new(@env, token, token.value)]
|
110
|
+
when Token::WILD
|
111
|
+
[WildcardSelector.new(@env, stream.next)]
|
112
|
+
when Token::LBRACKET
|
113
|
+
parse_bracketed_selection(stream)
|
114
|
+
else
|
115
|
+
[]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def parse_bracketed_selection(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
120
|
+
stream.expect Token::LBRACKET
|
121
|
+
segment_token = stream.next
|
122
|
+
|
123
|
+
selectors = []
|
124
|
+
|
125
|
+
loop do # rubocop:disable Metrics/BlockLength
|
126
|
+
case stream.peek.type
|
127
|
+
when Token::RBRACKET
|
128
|
+
break
|
129
|
+
when Token::INDEX
|
130
|
+
selectors << parse_index_or_slice(stream)
|
131
|
+
when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
|
132
|
+
token = stream.next
|
133
|
+
selectors << @name_selector.new(@env, token, decode_string_literal(token))
|
134
|
+
when Token::COLON
|
135
|
+
selectors << parse_slice_selector(stream)
|
136
|
+
when Token::WILD
|
137
|
+
selectors << WildcardSelector.new(@env, stream.next)
|
138
|
+
when Token::FILTER
|
139
|
+
selectors << parse_filter_selector(stream)
|
140
|
+
when Token::EOI
|
141
|
+
raise JSONPathSyntaxError.new("unexpected end of query", stream.next)
|
142
|
+
else
|
143
|
+
raise JSONPathSyntaxError.new("unexpected token in bracketed selection", stream.next)
|
144
|
+
end
|
145
|
+
|
146
|
+
case stream.peek.type
|
147
|
+
when Token::EOI
|
148
|
+
raise JSONPathSyntaxError.new("unexpected end of selector list", stream.next)
|
149
|
+
when Token::RBRACKET
|
150
|
+
break
|
151
|
+
else
|
152
|
+
stream.expect Token::COMMA
|
153
|
+
stream.next
|
154
|
+
stream.expect_not(Token::RBRACKET, "unexpected trailing comma")
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
stream.expect(Token::RBRACKET)
|
159
|
+
stream.next
|
160
|
+
|
161
|
+
raise JSONPathSyntaxError.new("empty segment", segment_token) if selectors.empty?
|
162
|
+
|
163
|
+
selectors
|
164
|
+
end
|
165
|
+
|
166
|
+
def parse_index_or_slice(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
167
|
+
token = stream.next
|
168
|
+
index = parse_i_json_int(token)
|
169
|
+
|
170
|
+
return @index_selector.new(@env, token, index) unless stream.peek.type == Token::COLON
|
171
|
+
|
172
|
+
stream.next # move past colon
|
173
|
+
stop = nil
|
174
|
+
step = nil
|
175
|
+
|
176
|
+
case stream.peek.type
|
177
|
+
when Token::INDEX
|
178
|
+
stop = parse_i_json_int(stream.next)
|
179
|
+
when Token::COLON
|
180
|
+
stream.next # move past colon
|
181
|
+
end
|
182
|
+
|
183
|
+
stream.next if stream.peek.type == Token::COLON
|
184
|
+
|
185
|
+
case stream.peek.type
|
186
|
+
when Token::INDEX
|
187
|
+
step = parse_i_json_int(stream.next)
|
188
|
+
when Token::RBRACKET
|
189
|
+
nil
|
190
|
+
else
|
191
|
+
error_token = stream.next
|
192
|
+
raise JSONPathSyntaxError.new("expected a slice, found '#{error_token.value}'", error_token)
|
193
|
+
end
|
194
|
+
|
195
|
+
SliceSelector.new(@env, token, index, stop, step)
|
196
|
+
end
|
197
|
+
|
198
|
+
def parse_slice_selector(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
199
|
+
stream.expect(Token::COLON)
|
200
|
+
token = stream.next
|
201
|
+
|
202
|
+
start = nil
|
203
|
+
stop = nil
|
204
|
+
step = nil
|
205
|
+
|
206
|
+
case stream.peek.type
|
207
|
+
when Token::INDEX
|
208
|
+
stop = parse_i_json_int(stream.next)
|
209
|
+
when Token::COLON
|
210
|
+
stream.next # move past colon
|
211
|
+
end
|
212
|
+
|
213
|
+
stream.next if stream.peek.type == Token::COLON
|
214
|
+
|
215
|
+
case stream.peek.type
|
216
|
+
when Token::INDEX
|
217
|
+
step = parse_i_json_int(stream.next)
|
218
|
+
when Token::RBRACKET
|
219
|
+
nil
|
220
|
+
else
|
221
|
+
error_token = stream.next
|
222
|
+
raise JSONPathSyntaxError.new("expected a slice, found '#{token.value}'", error_token)
|
223
|
+
end
|
224
|
+
|
225
|
+
SliceSelector.new(@env, token, start, stop, step)
|
226
|
+
end
|
227
|
+
|
228
|
+
def parse_filter_selector(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
229
|
+
token = stream.next
|
230
|
+
expression = parse_filter_expression(stream)
|
231
|
+
|
232
|
+
# Raise if expression must be compared.
|
233
|
+
if expression.is_a? FunctionExpression
|
234
|
+
func = @env.function_extensions[expression.name]
|
235
|
+
if func.class::RETURN_TYPE == ExpressionType::VALUE
|
236
|
+
raise JSONPathTypeError.new("result of #{expression.name}() must be compared", expression.token)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Raise if expression is a literal.
|
241
|
+
if expression.is_a? FilterExpressionLiteral
|
242
|
+
raise JSONPathSyntaxError.new("filter expression literals must be compared", expression.token)
|
243
|
+
end
|
244
|
+
|
245
|
+
FilterSelector.new(@env, token, FilterExpression.new(token, expression))
|
246
|
+
end
|
247
|
+
|
248
|
+
def parse_filter_expression(stream, precedence = Precedence::LOWEST) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
249
|
+
left = case stream.peek.type
|
250
|
+
when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
|
251
|
+
token = stream.next
|
252
|
+
StringLiteral.new(token, decode_string_literal(token))
|
253
|
+
when Token::FALSE
|
254
|
+
BooleanLiteral.new(stream.next, false)
|
255
|
+
when Token::TRUE
|
256
|
+
BooleanLiteral.new(stream.next, true)
|
257
|
+
when Token::FLOAT
|
258
|
+
parse_float_literal(stream)
|
259
|
+
when Token::FUNCTION
|
260
|
+
parse_function_expression(stream)
|
261
|
+
when Token::INT
|
262
|
+
parse_integer_literal(stream)
|
263
|
+
when Token::LPAREN
|
264
|
+
parse_grouped_expression(stream)
|
265
|
+
when Token::NOT
|
266
|
+
parse_prefix_expression(stream)
|
267
|
+
when Token::NULL
|
268
|
+
NullLiteral.new(stream.next, nil)
|
269
|
+
when Token::ROOT
|
270
|
+
parse_root_query(stream)
|
271
|
+
when Token::CURRENT
|
272
|
+
parse_relative_query(stream)
|
273
|
+
else
|
274
|
+
token = stream.next
|
275
|
+
raise JSONPathSyntaxError.new("unexpected '#{token.value}'", token)
|
276
|
+
end
|
277
|
+
|
278
|
+
loop do
|
279
|
+
peeked = stream.peek
|
280
|
+
if peeked.type == Token::EOI ||
|
281
|
+
peeked.type == Token::RBRACKET ||
|
282
|
+
PRECEDENCES.fetch(peeked.type, Precedence::LOWEST) < precedence
|
283
|
+
break
|
284
|
+
end
|
285
|
+
|
286
|
+
return left unless BINARY_OPERATORS.key?(peeked.type)
|
287
|
+
|
288
|
+
left = parse_infix_expression(stream, left)
|
289
|
+
end
|
290
|
+
|
291
|
+
left
|
292
|
+
end
|
293
|
+
|
294
|
+
def parse_integer_literal(stream)
|
295
|
+
token = stream.next
|
296
|
+
value = token.value
|
297
|
+
raise JSONPathSyntaxError.new("invalid integer literal", token) if value.start_with?("0") && value.length > 1
|
298
|
+
|
299
|
+
IntegerLiteral.new(token, Integer(Float(token.value)))
|
300
|
+
end
|
301
|
+
|
302
|
+
def parse_float_literal(stream)
|
303
|
+
token = stream.next
|
304
|
+
value = token.value
|
305
|
+
if value.start_with?("0") && value.split(".").first.length > 1
|
306
|
+
raise JSONPathSyntaxError.new("invalid float literal", token)
|
307
|
+
end
|
308
|
+
|
309
|
+
begin
|
310
|
+
FloatLiteral.new(token, Float(value))
|
311
|
+
rescue ArgumentError
|
312
|
+
raise JSONPathSyntaxError.new("invalid float literal", token)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def parse_function_expression(stream) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
317
|
+
token = stream.next
|
318
|
+
args = []
|
319
|
+
|
320
|
+
while stream.peek.type != Token::RPAREN
|
321
|
+
expr = case stream.peek.type
|
322
|
+
when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
|
323
|
+
arg_token = stream.next
|
324
|
+
StringLiteral.new(arg_token, decode_string_literal(arg_token))
|
325
|
+
when Token::FALSE
|
326
|
+
BooleanLiteral.new(stream.next, false)
|
327
|
+
when Token::TRUE
|
328
|
+
BooleanLiteral.new(stream.next, true)
|
329
|
+
when Token::FLOAT
|
330
|
+
parse_float_literal(stream)
|
331
|
+
when Token::FUNCTION
|
332
|
+
parse_function_expression(stream)
|
333
|
+
when Token::INT
|
334
|
+
parse_integer_literal(stream)
|
335
|
+
when Token::NULL
|
336
|
+
NullLiteral.new(stream.next, nil)
|
337
|
+
when Token::ROOT
|
338
|
+
parse_root_query(stream)
|
339
|
+
when Token::CURRENT
|
340
|
+
parse_relative_query(stream)
|
341
|
+
else
|
342
|
+
arg_token = stream.next
|
343
|
+
raise JSONPathSyntaxError.new("unexpected '#{arg_token.value}'", arg_token)
|
344
|
+
end
|
345
|
+
|
346
|
+
expr = parse_infix_expression(stream, expr) while BINARY_OPERATORS.key? stream.peek.type
|
347
|
+
|
348
|
+
args << expr
|
349
|
+
|
350
|
+
if stream.peek.type != Token::RPAREN
|
351
|
+
stream.expect(Token::COMMA)
|
352
|
+
stream.next
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
stream.expect(Token::RPAREN)
|
357
|
+
stream.next
|
358
|
+
|
359
|
+
validate_function_extension_signature(token, args)
|
360
|
+
FunctionExpression.new(token, token.value, args)
|
361
|
+
end
|
362
|
+
|
363
|
+
def parse_grouped_expression(stream)
|
364
|
+
stream.next # discard "("
|
365
|
+
expr = parse_filter_expression(stream)
|
366
|
+
|
367
|
+
while stream.peek.type != Token::RPAREN
|
368
|
+
raise JSONPathSyntaxError.new("unbalanced parentheses", stream.peek) if stream.peek.type == Token::EOI
|
369
|
+
|
370
|
+
expr = parse_infix_expression(stream, expr)
|
371
|
+
end
|
372
|
+
|
373
|
+
stream.expect(Token::RPAREN)
|
374
|
+
stream.next
|
375
|
+
expr
|
376
|
+
end
|
377
|
+
|
378
|
+
def parse_prefix_expression(stream)
|
379
|
+
token = stream.next
|
380
|
+
LogicalNotExpression.new(token, parse_filter_expression(stream, Precedence::PREFIX))
|
381
|
+
end
|
382
|
+
|
383
|
+
def parse_root_query(stream)
|
384
|
+
token = stream.next
|
385
|
+
RootQueryExpression.new(token, JSONPath.new(@env, parse_query(stream)))
|
386
|
+
end
|
387
|
+
|
388
|
+
def parse_relative_query(stream)
|
389
|
+
token = stream.next
|
390
|
+
RelativeQueryExpression.new(token, JSONPath.new(@env, parse_query(stream)))
|
391
|
+
end
|
392
|
+
|
393
|
+
def parse_infix_expression(stream, left) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
|
394
|
+
token = stream.next
|
395
|
+
precedence = PRECEDENCES.fetch(token.type, Precedence::LOWEST)
|
396
|
+
right = parse_filter_expression(stream, precedence)
|
397
|
+
|
398
|
+
if COMPARISON_OPERATORS.member? token.value
|
399
|
+
raise_for_non_comparable_function(left)
|
400
|
+
raise_for_non_comparable_function(right)
|
401
|
+
case token.type
|
402
|
+
when Token::EQ
|
403
|
+
EqExpression.new(token, left, right)
|
404
|
+
when Token::GE
|
405
|
+
GeExpression.new(token, left, right)
|
406
|
+
when Token::GT
|
407
|
+
GtExpression.new(token, left, right)
|
408
|
+
when Token::LE
|
409
|
+
LeExpression.new(token, left, right)
|
410
|
+
when Token::LT
|
411
|
+
LtExpression.new(token, left, right)
|
412
|
+
when Token::NE
|
413
|
+
NeExpression.new(token, left, right)
|
414
|
+
else
|
415
|
+
raise JSONPathSyntaxError.new("unexpected token", token)
|
416
|
+
end
|
417
|
+
else
|
418
|
+
raise_for_uncompared_literal(left)
|
419
|
+
raise_for_uncompared_literal(right)
|
420
|
+
case token.type
|
421
|
+
when Token::AND
|
422
|
+
LogicalAndExpression.new(token, left, right)
|
423
|
+
when Token::OR
|
424
|
+
LogicalOrExpression.new(token, left, right)
|
425
|
+
else
|
426
|
+
raise JSONPathSyntaxError.new("unexpected token", token)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def parse_i_json_int(token) # rubocop:disable Metrics/MethodLength
|
432
|
+
value = token.value
|
433
|
+
|
434
|
+
if value.length > 1 && value.start_with?("0", "-0")
|
435
|
+
raise JSONPathSyntaxError.new("invalid index '#{value}'", token)
|
436
|
+
end
|
437
|
+
|
438
|
+
begin
|
439
|
+
int = Integer(value)
|
440
|
+
rescue ArgumentError
|
441
|
+
raise JSONPathSyntaxError.new("invalid I-JSON integer", token)
|
442
|
+
end
|
443
|
+
|
444
|
+
if int < @env.class::MIN_INT_INDEX || int > @env.class::MAX_INT_INDEX
|
445
|
+
raise JSONPathSyntaxError.new("index out of range",
|
446
|
+
token)
|
447
|
+
end
|
448
|
+
|
449
|
+
int
|
450
|
+
end
|
451
|
+
|
452
|
+
def decode_string_literal(token)
|
453
|
+
if token.type == Token::SINGLE_QUOTE_STRING
|
454
|
+
JSONP3.unescape_string(token.value, "'", token)
|
455
|
+
else
|
456
|
+
JSONP3.unescape_string(token.value, '"', token)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
def raise_for_non_comparable_function(expression)
|
461
|
+
if expression.is_a?(QueryExpression) && !expression.query.singular?
|
462
|
+
raise JSONPathSyntaxError.new("non-singular query is not comparable", expression.token)
|
463
|
+
end
|
464
|
+
|
465
|
+
return unless expression.is_a?(FunctionExpression)
|
466
|
+
|
467
|
+
func = @env.function_extensions[expression.name]
|
468
|
+
return unless func.class::RETURN_TYPE != ExpressionType::VALUE
|
469
|
+
|
470
|
+
raise JSONPathTypeError.new("result of #{expression.name}() is not comparable", expression.token)
|
471
|
+
end
|
472
|
+
|
473
|
+
def raise_for_uncompared_literal(expression)
|
474
|
+
return unless expression.is_a? FilterExpressionLiteral
|
475
|
+
|
476
|
+
raise JSONPathSyntaxError.new("expression literals must be compared",
|
477
|
+
expression.token)
|
478
|
+
end
|
479
|
+
|
480
|
+
def validate_function_extension_signature(token, args) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
481
|
+
func = @env.function_extensions.fetch(token.value)
|
482
|
+
count = func.class::ARG_TYPES.length
|
483
|
+
|
484
|
+
unless args.length == count
|
485
|
+
raise JSONPathTypeError.new(
|
486
|
+
"#{token.value}() takes #{count} argument#{count == 1 ? "" : "s"} (#{args.length} given)",
|
487
|
+
token
|
488
|
+
)
|
489
|
+
end
|
490
|
+
|
491
|
+
func.class::ARG_TYPES.each_with_index do |t, i|
|
492
|
+
arg = args[i]
|
493
|
+
case t
|
494
|
+
when ExpressionType::VALUE
|
495
|
+
unless arg.is_a?(FilterExpressionLiteral) ||
|
496
|
+
(arg.is_a?(QueryExpression) && arg.query.singular?) ||
|
497
|
+
(function_return_type(arg) == ExpressionType::VALUE)
|
498
|
+
raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of ValueType", arg.token)
|
499
|
+
end
|
500
|
+
when ExpressionType::LOGICAL
|
501
|
+
unless arg.is_a?(QueryExpression) || arg.is_a?(InfixExpression)
|
502
|
+
raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of LogicalType", arg.token)
|
503
|
+
end
|
504
|
+
when ExpressionType::NODES
|
505
|
+
unless arg.is_a?(QueryExpression) || function_return_type(arg) == ExpressionType::NODES
|
506
|
+
raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of NodesType", arg.token)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|
510
|
+
rescue KeyError
|
511
|
+
raise JSONPathNameError.new("function '#{token.value}' is not defined", token)
|
512
|
+
end
|
513
|
+
|
514
|
+
def function_return_type(expression)
|
515
|
+
return nil unless expression.is_a? FunctionExpression
|
516
|
+
|
517
|
+
@env.function_extensions[expression.name].class::RETURN_TYPE
|
518
|
+
end
|
519
|
+
|
520
|
+
PRECEDENCES = {
|
521
|
+
Token::AND => Precedence::LOGICAL_AND,
|
522
|
+
Token::OR => Precedence::LOGICAL_OR,
|
523
|
+
Token::NOT => Precedence::PREFIX,
|
524
|
+
Token::EQ => Precedence::RELATIONAL,
|
525
|
+
Token::GE => Precedence::RELATIONAL,
|
526
|
+
Token::GT => Precedence::RELATIONAL,
|
527
|
+
Token::LE => Precedence::RELATIONAL,
|
528
|
+
Token::LT => Precedence::RELATIONAL,
|
529
|
+
Token::NE => Precedence::RELATIONAL,
|
530
|
+
Token::RPAREN => Precedence::LOWEST
|
531
|
+
}.freeze
|
532
|
+
|
533
|
+
BINARY_OPERATORS = {
|
534
|
+
Token::AND => "&&",
|
535
|
+
Token::OR => "||",
|
536
|
+
Token::EQ => "==",
|
537
|
+
Token::GE => ">=",
|
538
|
+
Token::GT => ">",
|
539
|
+
Token::LE => "<=",
|
540
|
+
Token::LT => "<",
|
541
|
+
Token::NE => "!="
|
542
|
+
}.freeze
|
543
|
+
|
544
|
+
COMPARISON_OPERATORS = Set[
|
545
|
+
"==",
|
546
|
+
">=",
|
547
|
+
">",
|
548
|
+
"<=",
|
549
|
+
"<",
|
550
|
+
"!=",
|
551
|
+
]
|
552
|
+
end
|
553
|
+
end
|
data/lib/json_p3/path.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "node"
|
4
|
+
|
5
|
+
module JSONP3
|
6
|
+
# A compiled JSONPath expression ready to be applied to JSON-like values.
|
7
|
+
class JSONPath
|
8
|
+
def initialize(env, segments)
|
9
|
+
@env = env
|
10
|
+
@segments = segments
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"$#{@segments.map(&:to_s).join}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Apply this JSONPath expression to JSON-like value _root_.
|
18
|
+
# @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
|
19
|
+
# @return [Array<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
|
20
|
+
def find(root)
|
21
|
+
nodes = [JSONPathNode.new(root, [], root)]
|
22
|
+
@segments.each { |segment| nodes = segment.resolve(nodes) }
|
23
|
+
JSONPathNodeList.new(nodes)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias apply find
|
27
|
+
|
28
|
+
# Return _true_ if this JSONPath expression is a singular query.
|
29
|
+
def singular?
|
30
|
+
@segments.each do |segment|
|
31
|
+
return false if segment.instance_of? RecursiveDescentSegment
|
32
|
+
return false unless segment.selectors.length == 1 && segment.selectors[0].singular?
|
33
|
+
end
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return _true_ if this JSONPath expression has no segments.
|
38
|
+
def empty?
|
39
|
+
@segments.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONP3
|
4
|
+
# Base class for all JSONPath segments.
|
5
|
+
class Segment
|
6
|
+
# @dynamic token, selectors
|
7
|
+
attr_reader :token, :selectors
|
8
|
+
|
9
|
+
def initialize(env, token, selectors)
|
10
|
+
@env = env
|
11
|
+
@token = token
|
12
|
+
@selectors = selectors
|
13
|
+
end
|
14
|
+
|
15
|
+
# Select the children of each node in _nodes_.
|
16
|
+
def resolve(_nodes)
|
17
|
+
raise "segments must implement resolve(nodes)"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The child selection segment.
|
22
|
+
class ChildSegment < Segment
|
23
|
+
def resolve(nodes)
|
24
|
+
rv = []
|
25
|
+
nodes.each do |node|
|
26
|
+
@selectors.each do |selector|
|
27
|
+
rv.concat selector.resolve(node)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rv
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"[#{@selectors.map(&:to_s).join(", ")}]"
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
self.class == other.class &&
|
39
|
+
@selectors == other.selectors &&
|
40
|
+
@token == other.token
|
41
|
+
end
|
42
|
+
|
43
|
+
alias eql? ==
|
44
|
+
|
45
|
+
def hash
|
46
|
+
[@selectors, @token].hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# The recursive descent segment
|
51
|
+
class RecursiveDescentSegment < Segment
|
52
|
+
def resolve(nodes)
|
53
|
+
rv = []
|
54
|
+
nodes.each do |node|
|
55
|
+
visit(node).each do |descendant|
|
56
|
+
@selectors.each do |selector|
|
57
|
+
rv.concat selector.resolve(descendant)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
rv
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
"..[#{@selectors.map(&:to_s).join(", ")}]"
|
66
|
+
end
|
67
|
+
|
68
|
+
def ==(other)
|
69
|
+
self.class == other.class &&
|
70
|
+
@selectors == other.selectors &&
|
71
|
+
@token == other.token
|
72
|
+
end
|
73
|
+
|
74
|
+
alias eql? ==
|
75
|
+
|
76
|
+
def hash
|
77
|
+
["..", @selectors, @token].hash
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def visit(node, depth = 1) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
83
|
+
raise JSONPathRecursionError.new("recursion limit exceeded", @token) if depth > @env.class::MAX_RECURSION_DEPTH
|
84
|
+
|
85
|
+
rv = [node]
|
86
|
+
|
87
|
+
if node.value.is_a? Array
|
88
|
+
node.value.each_with_index do |value, i|
|
89
|
+
child = JSONPathNode.new(value, [node.location, i], node.root)
|
90
|
+
rv.concat visit(child, depth + 1)
|
91
|
+
end
|
92
|
+
elsif node.value.is_a? Hash
|
93
|
+
node.value.each do |key, value|
|
94
|
+
child = JSONPathNode.new(value, [node.location, key], node.root)
|
95
|
+
rv.concat visit(child, depth + 1)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
rv
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|