janeway-jsonpath 0.1.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 +7 -0
- data/LICENSE +7 -0
- data/README.md +43 -0
- data/bin/janeway +130 -0
- data/lib/janeway/ast/array_slice_selector.rb +104 -0
- data/lib/janeway/ast/binary_operator.rb +101 -0
- data/lib/janeway/ast/boolean.rb +24 -0
- data/lib/janeway/ast/child_segment.rb +48 -0
- data/lib/janeway/ast/current_node.rb +71 -0
- data/lib/janeway/ast/descendant_segment.rb +44 -0
- data/lib/janeway/ast/error.rb +10 -0
- data/lib/janeway/ast/expression.rb +55 -0
- data/lib/janeway/ast/filter_selector.rb +61 -0
- data/lib/janeway/ast/function.rb +40 -0
- data/lib/janeway/ast/helpers.rb +27 -0
- data/lib/janeway/ast/identifier.rb +35 -0
- data/lib/janeway/ast/index_selector.rb +27 -0
- data/lib/janeway/ast/name_selector.rb +52 -0
- data/lib/janeway/ast/null.rb +23 -0
- data/lib/janeway/ast/number.rb +21 -0
- data/lib/janeway/ast/query.rb +41 -0
- data/lib/janeway/ast/root_node.rb +50 -0
- data/lib/janeway/ast/selector.rb +32 -0
- data/lib/janeway/ast/string_type.rb +21 -0
- data/lib/janeway/ast/unary_operator.rb +26 -0
- data/lib/janeway/ast/wildcard_selector.rb +46 -0
- data/lib/janeway/error.rb +23 -0
- data/lib/janeway/functions/count.rb +39 -0
- data/lib/janeway/functions/length.rb +33 -0
- data/lib/janeway/functions/match.rb +69 -0
- data/lib/janeway/functions/search.rb +63 -0
- data/lib/janeway/functions/value.rb +47 -0
- data/lib/janeway/functions.rb +62 -0
- data/lib/janeway/interpreter.rb +644 -0
- data/lib/janeway/lexer.rb +514 -0
- data/lib/janeway/location.rb +3 -0
- data/lib/janeway/parser.rb +608 -0
- data/lib/janeway/token.rb +39 -0
- data/lib/janeway/version.rb +5 -0
- data/lib/janeway.rb +51 -0
- metadata +92 -0
@@ -0,0 +1,608 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'functions'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
# Transform a list of tokens into an Abstract Syntax Tree
|
7
|
+
class Parser
|
8
|
+
class Error < Janeway::Error; end
|
9
|
+
|
10
|
+
attr_accessor :tokens, :ast
|
11
|
+
|
12
|
+
include Functions
|
13
|
+
|
14
|
+
UNARY_OPERATORS = %w[! -].freeze
|
15
|
+
BINARY_OPERATORS = %w[== != > < >= <= ,].freeze
|
16
|
+
LOGICAL_OPERATORS = %w[&& ||].freeze
|
17
|
+
|
18
|
+
LOWEST_PRECEDENCE = 0
|
19
|
+
PREFIX_PRECEDENCE = 7
|
20
|
+
OPERATOR_PRECEDENCE = {
|
21
|
+
',' => 0,
|
22
|
+
'||' => 1,
|
23
|
+
'&&' => 2,
|
24
|
+
'==' => 3,
|
25
|
+
'!=' => 3,
|
26
|
+
'>' => 4,
|
27
|
+
'<' => 4,
|
28
|
+
'>=' => 4,
|
29
|
+
'<=' => 4,
|
30
|
+
'(' => 8,
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
# @param query [String] jsonpath query to be lexed and parsed
|
34
|
+
#
|
35
|
+
# @return [AST]
|
36
|
+
def self.parse(query)
|
37
|
+
raise ArgumentError, "expect string, got #{query.inspect}" unless query.is_a?(String)
|
38
|
+
|
39
|
+
tokens = Janeway::Lexer.lex(query)
|
40
|
+
new(tokens).parse
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(tokens)
|
44
|
+
@tokens = tokens
|
45
|
+
@ast = AST::Query.new
|
46
|
+
@next_p = 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse
|
50
|
+
|
51
|
+
consume
|
52
|
+
@ast.root = parse_expr_recursively
|
53
|
+
consume
|
54
|
+
raise "unparsed tokens" unless current.type == :eof
|
55
|
+
|
56
|
+
@ast
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def build_token(type, lexeme = nil)
|
62
|
+
Token.new(type, lexeme, nil, nil)
|
63
|
+
end
|
64
|
+
|
65
|
+
def pending_tokens?
|
66
|
+
@next_p < tokens.length
|
67
|
+
end
|
68
|
+
|
69
|
+
def next_not_terminator?
|
70
|
+
next_token && next_token.type != :"\n" && next_token.type != :eof
|
71
|
+
end
|
72
|
+
|
73
|
+
# Make "next" token become "current" by moving the pointer
|
74
|
+
# @return [Token] consumed token
|
75
|
+
def consume(offset = 1)
|
76
|
+
t = lookahead(offset)
|
77
|
+
@next_p += offset
|
78
|
+
t
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return literal of current token. Consume.
|
82
|
+
# @return [String, Integer] literal value of token that is `current` when function is called.
|
83
|
+
def current_literal_and_consume
|
84
|
+
current.literal.tap { consume }
|
85
|
+
end
|
86
|
+
|
87
|
+
def consume_if_next_is(expected)
|
88
|
+
if next_token.type == expected.type
|
89
|
+
consume
|
90
|
+
true
|
91
|
+
else
|
92
|
+
unexpected_token_error(expected)
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def previous
|
98
|
+
lookahead(-1)
|
99
|
+
end
|
100
|
+
|
101
|
+
def current
|
102
|
+
lookahead(0)
|
103
|
+
end
|
104
|
+
|
105
|
+
def next_token
|
106
|
+
lookahead
|
107
|
+
end
|
108
|
+
|
109
|
+
def lookahead(offset = 1)
|
110
|
+
lookahead_p = (@next_p - 1) + offset
|
111
|
+
return nil if lookahead_p.negative? || lookahead_p >= tokens.length
|
112
|
+
|
113
|
+
tokens[lookahead_p]
|
114
|
+
end
|
115
|
+
|
116
|
+
def current_precedence
|
117
|
+
OPERATOR_PRECEDENCE[current.lexeme] || LOWEST_PRECEDENCE
|
118
|
+
end
|
119
|
+
|
120
|
+
def next_precedence
|
121
|
+
OPERATOR_PRECEDENCE[next_token.lexeme] || LOWEST_PRECEDENCE
|
122
|
+
end
|
123
|
+
|
124
|
+
def unexpected_token_error(expected = nil)
|
125
|
+
if expected
|
126
|
+
raise Error, "Unexpected token #{current.lexeme.inspect} (expected #{expected.inspect}) (next is #{next_token.inspect})"
|
127
|
+
else
|
128
|
+
raise Error, "Unexpected token #{current.lexeme.inspect} (next is #{next_token.inspect})"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_syntax_compliance(ast_node)
|
133
|
+
return if ast_node.expects?(next_token)
|
134
|
+
|
135
|
+
unexpected_token_error
|
136
|
+
end
|
137
|
+
|
138
|
+
def determine_parsing_function
|
139
|
+
parse_methods = %I[identifier number string true false nil function if while root current_node]
|
140
|
+
if parse_methods.include?(current.type)
|
141
|
+
:"parse_#{current.type}"
|
142
|
+
elsif current.type == :group_start # (
|
143
|
+
:parse_grouped_expr
|
144
|
+
elsif %I[\n eof].include?(current.type)
|
145
|
+
:parse_terminator
|
146
|
+
elsif UNARY_OPERATORS.include?(current.lexeme)
|
147
|
+
:parse_unary_operator
|
148
|
+
elsif current.type == :child_start # [
|
149
|
+
:parse_child_segment
|
150
|
+
elsif current.type == :dot # .
|
151
|
+
:parse_dot_notation
|
152
|
+
elsif current.type == :descendants # ..
|
153
|
+
:parse_descendant_segment
|
154
|
+
elsif current.type == :filter # ?
|
155
|
+
:parse_filter_selector
|
156
|
+
elsif current.type == :null # null
|
157
|
+
:parse_null
|
158
|
+
else
|
159
|
+
raise "Don't know how to parse #{current}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [nil, Symbol]
|
164
|
+
def determine_infix_function(token = current)
|
165
|
+
return unless (BINARY_OPERATORS + LOGICAL_OPERATORS).include?(token.lexeme)
|
166
|
+
|
167
|
+
:parse_binary_operator
|
168
|
+
end
|
169
|
+
|
170
|
+
def parse_identifier
|
171
|
+
ident = AST::Identifier.new(current.lexeme)
|
172
|
+
check_syntax_compliance(ident)
|
173
|
+
ident
|
174
|
+
end
|
175
|
+
|
176
|
+
def parse_string
|
177
|
+
AST::StringType.new(current.literal)
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse_number
|
181
|
+
AST::Number.new(current.literal)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Consume minus operator and apply it to the (expected) number token following it.
|
185
|
+
# Don't consume the number token.
|
186
|
+
def parse_minus_operator
|
187
|
+
raise "Expect token '-', got #{current.lexeme.inspect}" unless current.type == :minus
|
188
|
+
|
189
|
+
# RFC: negative 0 is allowed within a filter selector comparison, but is NOT allowed within an index selector or array slice selector.
|
190
|
+
# Detect that condition here
|
191
|
+
if next_token.type == :number && next_token.literal == 0
|
192
|
+
if [previous.type, lookahead(2).type].any? { _1 == :array_slice_separator}
|
193
|
+
raise Error, 'Negative zero is not allowed in an array slice selector'
|
194
|
+
elsif %i[union child_start].include?(previous.type)
|
195
|
+
raise Error, 'Negative zero is not allowed in an index selector'
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# '-' must be followed by a number token.
|
200
|
+
# Parse number and apply - sign to its literal value
|
201
|
+
consume
|
202
|
+
parse_number
|
203
|
+
current.literal *= -1
|
204
|
+
current
|
205
|
+
end
|
206
|
+
|
207
|
+
# @return [AST::Null]
|
208
|
+
def parse_null
|
209
|
+
AST::Null.new
|
210
|
+
end
|
211
|
+
|
212
|
+
def parse_boolean
|
213
|
+
AST::Boolean.new(current.literal == 'true')
|
214
|
+
end
|
215
|
+
|
216
|
+
# Parse a descendant segment.
|
217
|
+
#
|
218
|
+
# The descendant segment consists of a double dot "..", followed by
|
219
|
+
# a child segment (using bracket notation).
|
220
|
+
#
|
221
|
+
# Shorthand notations are also provided that correspond to the shorthand forms of the child segment.
|
222
|
+
#
|
223
|
+
# descendant-segment = ".." (bracketed-selection /
|
224
|
+
# wildcard-selector /
|
225
|
+
# member-name-shorthand)
|
226
|
+
# ..*, the descendant-segment directly built from a
|
227
|
+
# wildcard-selector, is shorthand for ..[*].
|
228
|
+
#
|
229
|
+
# ..<member-name>, a descendant-segment built from a
|
230
|
+
# member-name-shorthand, is shorthand for ..['<member-name>'].
|
231
|
+
# Note: as with the similar shorthand of a child-segment, this can
|
232
|
+
# only be used with member names that are composed of certain
|
233
|
+
# characters, as specified in the ABNF rule member-name-shorthand.
|
234
|
+
#
|
235
|
+
# Note: .. on its own is not a valid segment.
|
236
|
+
def parse_descendant_segment
|
237
|
+
consume # '..'
|
238
|
+
|
239
|
+
# DescendantSegment must be followed by a selector S which it applies to all descendants.
|
240
|
+
#
|
241
|
+
# Normally the parser makes the selector after S be a child of S.
|
242
|
+
# However that is not the desired behavior for DescendantSelector.
|
243
|
+
# Consider '$.a..b[1]'. The first element must be taken from the set of all 'b' keys.
|
244
|
+
# If the ChildSegment was a child of the `b` NameSelector, then it would be taking
|
245
|
+
# index 1 from every 'b' found rather than from the set of all 'b's.
|
246
|
+
#
|
247
|
+
# To get around this, the Parser must embed a Selector object that
|
248
|
+
# doesn't include the following selector as a child. Then the following
|
249
|
+
# selector must be made a child of the DescendantSegment.
|
250
|
+
selector =
|
251
|
+
case next_token.type
|
252
|
+
when :wildcard then parse_wildcard_selector(and_child: false)
|
253
|
+
when :child_start then parse_child_segment(and_child: false)
|
254
|
+
when :string, :identifier then parse_name_selector(and_child: false)
|
255
|
+
else
|
256
|
+
raise "Invalid query: descendant segment must have selector, got ..#{next_token.type}"
|
257
|
+
end
|
258
|
+
|
259
|
+
AST::DescendantSegment.new(selector).tap do |ds|
|
260
|
+
# If there is another selector after this one, make it a child
|
261
|
+
ds.child = parse_next_selector
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Dot notation reprsents a name selector, and is an alternative to bracket notation.
|
266
|
+
# These examples are equivalent:
|
267
|
+
# $.store
|
268
|
+
# $[store]
|
269
|
+
#
|
270
|
+
# RFC9535 grammar specifies that the dot may be followed by only 2 selector types:
|
271
|
+
# * WildCardSelector ("*")
|
272
|
+
# * member name (with only certain chars useable. For example, names containing dots are not allowed here.)
|
273
|
+
def parse_dot_notation
|
274
|
+
consume # "."
|
275
|
+
raise "#parse_dot_notation expects to consume :dot, got #{current}" unless current.type == :dot
|
276
|
+
|
277
|
+
|
278
|
+
case next_token.type
|
279
|
+
# FIXME: implement a different name lexer which is limited to only the chars allowed under dot notation
|
280
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.5.1.1
|
281
|
+
when :identifier then parse_name_selector
|
282
|
+
when :wildcard then parse_wildcard_selector
|
283
|
+
else
|
284
|
+
raise "cannot parse #{current.type}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def parse_grouped_expr
|
289
|
+
consume
|
290
|
+
|
291
|
+
expr = parse_expr_recursively
|
292
|
+
return unless consume_if_next_is(build_token(:group_end, ')'))
|
293
|
+
|
294
|
+
expr
|
295
|
+
end
|
296
|
+
|
297
|
+
# TODO: Temporary impl; reflect more deeply about the appropriate way of parsing a terminator.
|
298
|
+
def parse_terminator
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
|
302
|
+
def parse_root
|
303
|
+
|
304
|
+
# detect optional following selector
|
305
|
+
selector =
|
306
|
+
case next_token.type
|
307
|
+
when :dot then parse_dot_notation
|
308
|
+
when :child_start then parse_child_segment
|
309
|
+
when :descendants then parse_descendant_segment
|
310
|
+
end
|
311
|
+
|
312
|
+
AST::RootNode.new(selector)
|
313
|
+
end
|
314
|
+
|
315
|
+
# Parse the current node operator "@", and optionally a selector which is applied to it
|
316
|
+
def parse_current_node
|
317
|
+
|
318
|
+
# detect optional following selector
|
319
|
+
selector =
|
320
|
+
case next_token.type
|
321
|
+
when :dot then parse_dot_notation
|
322
|
+
when :child_start then parse_child_segment
|
323
|
+
when :descendants then parse_descendant_segment
|
324
|
+
end
|
325
|
+
|
326
|
+
AST::CurrentNode.new(selector)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Parse one or more selectors surrounded by parentheses.
|
330
|
+
#
|
331
|
+
# More than 1 selector may be within the parentheses, as long as they are separated by commas.
|
332
|
+
# If multiple selectors are given, then their results will be combined during the
|
333
|
+
# interpretation stage.
|
334
|
+
#
|
335
|
+
# The selectors may be any of these types:
|
336
|
+
# * name selector, eg. 'name', selects a named child of an object.
|
337
|
+
# * index selector, eg. 3, selects an indexed child of an array.
|
338
|
+
# * wildcard selector, eg. * selects all children of a node and in the expression ..[*]
|
339
|
+
# selects all descendants of a node.
|
340
|
+
# * array slice selector selects a series of elements from an array, giving a start position,
|
341
|
+
# an end position, and an optional step value that moves the position from the start to the end.
|
342
|
+
# * filter expressions select certain children of an object or array, as in:
|
343
|
+
#
|
344
|
+
# When there is only a single selector in the list, parse and return that selector only.
|
345
|
+
# When there are multiple selectors, create a ChildSegment that contains all the selectors.
|
346
|
+
#
|
347
|
+
# This is not just a speed optimization. Serial selectors that feed into
|
348
|
+
# each other have different behaviour than serial child segments.
|
349
|
+
#
|
350
|
+
# @param and_child [Boolean] make following token a child of this selector list
|
351
|
+
# @return [AST::ChildSegment]
|
352
|
+
def parse_child_segment(and_child: true)
|
353
|
+
consume
|
354
|
+
raise "Expect token [, got #{current.lexeme.inspect}" unless current.type == :child_start
|
355
|
+
|
356
|
+
consume # "["
|
357
|
+
|
358
|
+
child_segment = AST::ChildSegment.new
|
359
|
+
loop do
|
360
|
+
selector = parse_selector
|
361
|
+
child_segment << selector if selector # nil selector means empty brackets
|
362
|
+
|
363
|
+
break unless current.type == :union # no more selectors in these parentheses
|
364
|
+
|
365
|
+
# consume union operator and move on to next selector
|
366
|
+
consume # ","
|
367
|
+
|
368
|
+
# not allowed to have comma with nothing after it
|
369
|
+
if current.type == :child_end
|
370
|
+
raise Error.new("Comma must be followed by another expression in filter selector")
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Do not consume the final ']', the top-level parsing loop will eat that
|
375
|
+
unless current.type == :child_end
|
376
|
+
# developer error, check the parsing function
|
377
|
+
raise "expect current token to be ], got #{current.type.inspect}"
|
378
|
+
end
|
379
|
+
|
380
|
+
# if the child_segment contains just one selector, then return the selector instead.
|
381
|
+
# This way a series of selectors feed results to each other without
|
382
|
+
# combining results in a node list.
|
383
|
+
node =
|
384
|
+
case child_segment.size
|
385
|
+
when 0 then raise Error.new('Empty child segment')
|
386
|
+
when 1 then child_segment.first
|
387
|
+
else child_segment
|
388
|
+
end
|
389
|
+
|
390
|
+
if and_child
|
391
|
+
# Parse any subsequent expression which consumes this child segment
|
392
|
+
node.child = parse_next_selector
|
393
|
+
end
|
394
|
+
|
395
|
+
node
|
396
|
+
end
|
397
|
+
|
398
|
+
# Parse a selector and return it.
|
399
|
+
# @return [Selector, ChildSegment, nil] nil if no more selectors to use
|
400
|
+
def parse_next_selector
|
401
|
+
case next_token.type
|
402
|
+
when :child_start then parse_child_segment
|
403
|
+
when :dot then parse_dot_notation
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Return true if the given token represents the start of any type of selector,
|
408
|
+
# or a collection of selectors.
|
409
|
+
#
|
410
|
+
# @param token [Token]
|
411
|
+
# @return [Boolean]
|
412
|
+
def selector?(token)
|
413
|
+
type = token.type.to_s
|
414
|
+
type.include?('selector') || %w[dot child_start].include?(type)
|
415
|
+
end
|
416
|
+
|
417
|
+
# Parse a selector which is inside brackets
|
418
|
+
def parse_selector
|
419
|
+
case current.type
|
420
|
+
when :array_slice_separator then parse_array_slice_selector
|
421
|
+
when :filter then parse_filter_selector
|
422
|
+
when :wildcard then parse_wildcard_selector
|
423
|
+
when :minus
|
424
|
+
# apply the - sign to the following number and retry
|
425
|
+
parse_minus_operator
|
426
|
+
parse_selector
|
427
|
+
when :number
|
428
|
+
if lookahead.type == :array_slice_separator
|
429
|
+
parse_array_slice_selector
|
430
|
+
else
|
431
|
+
AST::IndexSelector.new(current_literal_and_consume)
|
432
|
+
end
|
433
|
+
when :identifier, :string
|
434
|
+
AST::NameSelector.new(current_literal_and_consume)
|
435
|
+
when :child_end then nil # empty brackets, do nothing.
|
436
|
+
else
|
437
|
+
raise "Unhandled selector: #{current}"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Parse wildcard selector and any following selector
|
442
|
+
# @param and_child [Boolean] make following token a child of this selector
|
443
|
+
def parse_wildcard_selector(and_child: true)
|
444
|
+
selector = AST::WildcardSelector.new
|
445
|
+
consume
|
446
|
+
selector.child = parse_next_selector if and_child
|
447
|
+
selector
|
448
|
+
end
|
449
|
+
|
450
|
+
# An array slice start:end:step selects a series of elements from
|
451
|
+
# an array, giving a start position, an end position, and an optional step
|
452
|
+
# value that moves the position from the start to the end.
|
453
|
+
#
|
454
|
+
# @example
|
455
|
+
# $[1:3]
|
456
|
+
# $[5:]
|
457
|
+
# $[1:5:2]
|
458
|
+
# $[5:1:-2]
|
459
|
+
# $[::-1]
|
460
|
+
#
|
461
|
+
# @return [AST::ArraySliceSelector]
|
462
|
+
def parse_array_slice_selector
|
463
|
+
start, end_, step = Array.new(3) { parse_array_slice_component }.map { _1&.literal }
|
464
|
+
|
465
|
+
|
466
|
+
raise "After array slice, expect ], got #{current.lexeme}" unless current.type == :child_end # ]
|
467
|
+
|
468
|
+
AST::ArraySliceSelector.new(start, end_, step)
|
469
|
+
end
|
470
|
+
|
471
|
+
# Extract the number from an array slice selector.
|
472
|
+
# Consume up to and including the next : token.
|
473
|
+
# If no number is found, return nil.
|
474
|
+
# @return [Number, nil] nil if the start is implicit
|
475
|
+
def parse_array_slice_component
|
476
|
+
token =
|
477
|
+
case current.type
|
478
|
+
when :array_slice_separator, :child_end, :union then nil
|
479
|
+
when :minus # apply - sign to number and retry
|
480
|
+
parse_minus_operator
|
481
|
+
parse_array_slice_component
|
482
|
+
when :number then current
|
483
|
+
else raise "Unexpected token in array slice selector: #{current}"
|
484
|
+
end
|
485
|
+
consume if current.type == :number
|
486
|
+
consume if current.type == :array_slice_separator
|
487
|
+
token
|
488
|
+
end
|
489
|
+
|
490
|
+
# Parse a name selector.
|
491
|
+
# The name selector may have been in dot notation or parentheses, that part is already parsed.
|
492
|
+
# Next token is just the name.
|
493
|
+
#
|
494
|
+
# @param and_child [Boolean] make following token a child of this selector
|
495
|
+
# @return [AST::NameSelector]
|
496
|
+
def parse_name_selector(and_child: true)
|
497
|
+
consume
|
498
|
+
selector = AST::NameSelector.new(current.lexeme)
|
499
|
+
if and_child
|
500
|
+
# If there is a following expression, parse that too
|
501
|
+
case next_token.type
|
502
|
+
when :dot then selector.child = parse_dot_notation
|
503
|
+
when :child_start then selector.child = parse_child_segment
|
504
|
+
when :descendants then selector.child = parse_descendant_segment
|
505
|
+
end
|
506
|
+
end
|
507
|
+
selector
|
508
|
+
end
|
509
|
+
|
510
|
+
# Feed tokens to the FilterSelector until hitting a terminator
|
511
|
+
def parse_filter_selector
|
512
|
+
|
513
|
+
selector = AST::FilterSelector.new
|
514
|
+
terminator_types = %I[child_end union eof]
|
515
|
+
while next_token && !terminator_types.include?(next_token.type)
|
516
|
+
consume
|
517
|
+
node =
|
518
|
+
if BINARY_OPERATORS.include?(current.lexeme)
|
519
|
+
parse_binary_operator(selector.value)
|
520
|
+
else
|
521
|
+
parse_expr_recursively
|
522
|
+
end
|
523
|
+
|
524
|
+
# may replace existing node with a binary operator that incorporates the original node
|
525
|
+
selector.value = node
|
526
|
+
end
|
527
|
+
|
528
|
+
# Check for literal, they are not allowed to be a complete condition in a filter selector
|
529
|
+
if selector.value.literal?
|
530
|
+
raise Error, "Literal #{selector.value} must be used within a comparison"
|
531
|
+
end
|
532
|
+
|
533
|
+
consume
|
534
|
+
|
535
|
+
selector
|
536
|
+
end
|
537
|
+
|
538
|
+
# @return [AST::UnaryOperator, AST::Number]
|
539
|
+
def parse_unary_operator
|
540
|
+
case current.type
|
541
|
+
when :not then parse_not_operator
|
542
|
+
when :minus
|
543
|
+
parse_minus_operator
|
544
|
+
parse_number
|
545
|
+
else
|
546
|
+
raise "unknown unary operator: #{current.inspect}"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
def parse_not_operator
|
551
|
+
AST::UnaryOperator.new(current.type).tap do |op|
|
552
|
+
consume
|
553
|
+
op.operand = parse_expr_recursively(PREFIX_PRECEDENCE)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
def parse_binary_operator(left)
|
558
|
+
op = AST::BinaryOperator.new(current.type, left)
|
559
|
+
op_precedence = current_precedence
|
560
|
+
|
561
|
+
consume
|
562
|
+
op.right = parse_expr_recursively(op_precedence)
|
563
|
+
|
564
|
+
op
|
565
|
+
end
|
566
|
+
|
567
|
+
# Parse a JSONPath function call
|
568
|
+
def parse_function
|
569
|
+
parsing_function = "parse_function_#{current.literal}"
|
570
|
+
result = send(parsing_function)
|
571
|
+
result
|
572
|
+
end
|
573
|
+
|
574
|
+
# Parse an expression
|
575
|
+
def parse_expr
|
576
|
+
parsing_function = determine_parsing_function
|
577
|
+
raise Error, "Unrecognized token: #{current.lexeme.inspect}" unless parsing_function
|
578
|
+
|
579
|
+
send(parsing_function)
|
580
|
+
end
|
581
|
+
|
582
|
+
def parse_expr_recursively(precedence = LOWEST_PRECEDENCE)
|
583
|
+
parsing_function = determine_parsing_function
|
584
|
+
raise Error, "Unrecognized token: #{current.lexeme.inspect}" unless parsing_function
|
585
|
+
|
586
|
+
tk = current
|
587
|
+
expr = send(parsing_function)
|
588
|
+
return unless expr # When expr is nil, it means we have reached a \n or a eof.
|
589
|
+
|
590
|
+
# Note that here we are checking the NEXT token.
|
591
|
+
if next_not_terminator? && precedence < next_precedence
|
592
|
+
end
|
593
|
+
while next_not_terminator? && precedence < next_precedence
|
594
|
+
infix_parsing_function = determine_infix_function(next_token)
|
595
|
+
|
596
|
+
return expr if infix_parsing_function.nil?
|
597
|
+
|
598
|
+
consume
|
599
|
+
expr = send(infix_parsing_function, expr)
|
600
|
+
end
|
601
|
+
|
602
|
+
expr
|
603
|
+
end
|
604
|
+
|
605
|
+
alias parse_true parse_boolean
|
606
|
+
alias parse_false parse_boolean
|
607
|
+
end
|
608
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
class Token
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :type, :lexeme, :location
|
10
|
+
|
11
|
+
# write-access so '-' operator can modify number value
|
12
|
+
attr_accessor :literal
|
13
|
+
|
14
|
+
def_delegators :@location, :line, :col, :length
|
15
|
+
|
16
|
+
def initialize(type, lexeme, literal, location)
|
17
|
+
@type = type
|
18
|
+
@lexeme = lexeme
|
19
|
+
@literal = literal
|
20
|
+
@location = location
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"Token<#{@type}: #{@lexeme.inspect}>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
# This is intended to make unit test expectations simple to define, experimental, may drop
|
29
|
+
case other
|
30
|
+
when Integer, String then @literal == other
|
31
|
+
when Symbol then @type == other
|
32
|
+
when Token
|
33
|
+
@type == other.type && @lexeme == other.lexem && @literal = other.literal
|
34
|
+
else
|
35
|
+
raise ArgumentError, "don't know how to compare Token with #{other.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/janeway.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
# Janeway jsonpath parsing library
|
6
|
+
module Janeway
|
7
|
+
# Abstract Syntax Tree
|
8
|
+
module AST
|
9
|
+
end
|
10
|
+
|
11
|
+
# Apply a JsonPath query to the input, and return the result.
|
12
|
+
#
|
13
|
+
# @param query [String] jsonpath query
|
14
|
+
# @param input [Object] ruby object to be searched
|
15
|
+
# @return [Array] all matched objects
|
16
|
+
def self.find_all(query, input)
|
17
|
+
query = compile(query)
|
18
|
+
Janeway::Interpreter.new(input).interpret(query)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Compile a JsonPath query into an Abstract Syntax Tree.
|
22
|
+
#
|
23
|
+
# This can be used and re-used later on multiple inputs.
|
24
|
+
#
|
25
|
+
# @param query [String] jsonpath query
|
26
|
+
# @return [Janeway::AST::Query]
|
27
|
+
def self.compile(query)
|
28
|
+
Janeway::Parser.parse(query)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Require ruby source files in the given dir. Do not recurse to subdirs.
|
33
|
+
# @param dir [String] dir path relative to __dir__
|
34
|
+
# @return [void]
|
35
|
+
def require_libs(dir)
|
36
|
+
absolute_path = File.join(__dir__, dir)
|
37
|
+
raise "No such dir: #{dir.inspect}" unless File.directory?(absolute_path)
|
38
|
+
|
39
|
+
Dir.children(absolute_path).sort.each do |filename|
|
40
|
+
next if File.directory?(File.join(absolute_path, filename))
|
41
|
+
|
42
|
+
rel_path = File.join(dir, filename)
|
43
|
+
require_relative(rel_path[0..-4]) # omits ".rb" extension
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# These are dependencies of the other AST source files, and must come first
|
48
|
+
require_relative 'janeway/ast/expression'
|
49
|
+
|
50
|
+
require_libs('janeway/ast')
|
51
|
+
require_libs('janeway')
|