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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +43 -0
  4. data/bin/janeway +130 -0
  5. data/lib/janeway/ast/array_slice_selector.rb +104 -0
  6. data/lib/janeway/ast/binary_operator.rb +101 -0
  7. data/lib/janeway/ast/boolean.rb +24 -0
  8. data/lib/janeway/ast/child_segment.rb +48 -0
  9. data/lib/janeway/ast/current_node.rb +71 -0
  10. data/lib/janeway/ast/descendant_segment.rb +44 -0
  11. data/lib/janeway/ast/error.rb +10 -0
  12. data/lib/janeway/ast/expression.rb +55 -0
  13. data/lib/janeway/ast/filter_selector.rb +61 -0
  14. data/lib/janeway/ast/function.rb +40 -0
  15. data/lib/janeway/ast/helpers.rb +27 -0
  16. data/lib/janeway/ast/identifier.rb +35 -0
  17. data/lib/janeway/ast/index_selector.rb +27 -0
  18. data/lib/janeway/ast/name_selector.rb +52 -0
  19. data/lib/janeway/ast/null.rb +23 -0
  20. data/lib/janeway/ast/number.rb +21 -0
  21. data/lib/janeway/ast/query.rb +41 -0
  22. data/lib/janeway/ast/root_node.rb +50 -0
  23. data/lib/janeway/ast/selector.rb +32 -0
  24. data/lib/janeway/ast/string_type.rb +21 -0
  25. data/lib/janeway/ast/unary_operator.rb +26 -0
  26. data/lib/janeway/ast/wildcard_selector.rb +46 -0
  27. data/lib/janeway/error.rb +23 -0
  28. data/lib/janeway/functions/count.rb +39 -0
  29. data/lib/janeway/functions/length.rb +33 -0
  30. data/lib/janeway/functions/match.rb +69 -0
  31. data/lib/janeway/functions/search.rb +63 -0
  32. data/lib/janeway/functions/value.rb +47 -0
  33. data/lib/janeway/functions.rb +62 -0
  34. data/lib/janeway/interpreter.rb +644 -0
  35. data/lib/janeway/lexer.rb +514 -0
  36. data/lib/janeway/location.rb +3 -0
  37. data/lib/janeway/parser.rb +608 -0
  38. data/lib/janeway/token.rb +39 -0
  39. data/lib/janeway/version.rb +5 -0
  40. data/lib/janeway.rb +51 -0
  41. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Janeway
4
+ VERSION = '0.1.0'
5
+ 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')