Class: JSONP3::Path::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/json_p3/path/parser.rb

Overview

JSONPath query parser.

Defined Under Namespace

Classes: Precedence

Constant Summary collapse

PRECEDENCES =
{
  token_and: Precedence::LOGICAL_AND,
  token_or: Precedence::LOGICAL_OR,
  token_not: Precedence::PREFIX,
  token_eq: Precedence::RELATIONAL,
  token_ge: Precedence::RELATIONAL,
  token_gt: Precedence::RELATIONAL,
  token_le: Precedence::RELATIONAL,
  token_lt: Precedence::RELATIONAL,
  token_ne: Precedence::RELATIONAL,
  token_rparen: Precedence::LOWEST
}.freeze
BINARY_OPERATORS =
{
  token_and: "&&",
  token_or: "||",
  token_eq: "==",
  token_ge: ">=",
  token_gt: ">",
  token_le: "<=",
  token_lt: "<",
  token_ne: "!="
}.freeze
COMPARISON_OPERATORS =
Set[
  :token_eq,
  :token_ge,
  :token_gt,
  :token_le,
  :token_lt,
  :token_ne
]

Instance Method Summary collapse

Constructor Details

#initialize(env, query, tokens) ⇒ Parser

Returns a new instance of Parser.

Parameters:

  • env (JSONPathEnvironment)
  • query (String)
  • tokens (Array[t_token])


60
61
62
63
64
65
66
# File 'lib/json_p3/path/parser.rb', line 60

def initialize(env, query, tokens)
  @env = env
  @query = query
  @tokens = tokens
  @pos = 0
  @eoi = [:token_eoi, query.size, query.size] #: t_token
end

Instance Method Details

#eat(kind, message = nil) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/json_p3/path/parser.rb', line 77

def eat(kind, message = nil)
  token = self.next
  unless token.first == kind
    raise SyntaxError.new(
      message || "expected #{kind}, found #{token.first}",
      token,
      @query
    )
  end

  token
end

#function_return_type(expression) ⇒ Object



669
670
671
672
673
# File 'lib/json_p3/path/parser.rb', line 669

def function_return_type(expression)
  return nil unless expression.is_a? FunctionExpression

  expression.func.class::RETURN_TYPE
end

#kindObject



94
# File 'lib/json_p3/path/parser.rb', line 94

def kind = (@tokens[@pos] || @eoi).first

#nextObject



68
69
70
71
72
73
74
75
# File 'lib/json_p3/path/parser.rb', line 68

def next
  if (token = @tokens[@pos])
    @pos += 1
    token
  else
    @eoi
  end
end

#parseObject



97
98
99
100
101
102
# File 'lib/json_p3/path/parser.rb', line 97

def parse
  eat(:token_dollar)
  segments = parse_segments
  eat(:token_eoi)
  segments
end

#parse_absolute_queryObject



545
546
547
548
# File 'lib/json_p3/path/parser.rb', line 545

def parse_absolute_query
  token = eat(:token_dollar)
  AbsoluteQueryExpression.new(token, Query.new(@env, parse_segments))
end

#parse_bracketed_selectorsObject



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/json_p3/path/parser.rb', line 180

def parse_bracketed_selectors
  segment_token = eat(:token_lbracket)
  selectors = [] #: Array[Selector]

  loop do
    skip(:token_trivia)

    case peek.first
    when :token_rbracket
      break
    when :token_index
      selectors << parse_index_or_slice
    when :token_double_quoted_string, :token_single_quoted_string
      token = self.next
      selectors << @env.class::NAME_SELECTOR.new(
        @env,
        token,
        JSONP3::Path.get_token_value(token, @query)
      )
    when :token_double_quoted_esc_string, :token_single_quoted_esc_string
      token = self.next
      selectors << @env.class::NAME_SELECTOR.new(
        @env,
        token,
        JSONP3::Path.unescape(
          JSONP3::Path.get_token_value(token, @query), token, @query
        )
      )
    when :token_colon
      selectors << parse_slice_selector
    when :token_asterisk
      selectors << WildcardSelector.new(@env, self.next)
    when :token_question
      selectors << parse_filter_selector
    when :token_eoi
      raise SyntaxError.new(
        "unexpected end of query",
        peek,
        @query
      )
    else
      raise SyntaxError.new(
        "unexpected token #{JSONP3::Path.get_token_value(peek, @query).inspect}",
        self.next,
        @query
      )
    end

    skip(:token_trivia)

    case peek.first
    when :token_eoi
      raise SyntaxError.new(
        "unexpected end of query",
        peek,
        @query
      )
    when :token_rbracket
      break
    else
      eat(:token_comma)
      if peek.first == :token_rbracket
        raise SyntaxError.new(
          "unexpected trailing comma",
          peek,
          @query
        )
      end
    end
  end

  skip(:token_trivia)
  eat(:token_rbracket)

  if selectors.empty?
    raise SyntaxError.new(
      "unexpected empty segment",
      segment_token,
      @query
    )
  end

  selectors
end

#parse_descendant_selectorsObject



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/json_p3/path/parser.rb', line 144

def parse_descendant_selectors
  case peek.first
  when :token_name, :token_asterisk
    [parse_shorthand_selector]
  when :token_lbracket
    parse_bracketed_selectors
  else
    raise SyntaxError.new(
      "expected a selector",
      peek,
      @query
    )
  end
end

#parse_filter_expression(precedence) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/json_p3/path/parser.rb', line 320

def parse_filter_expression(precedence)
  left = parse_primary

  loop do
    skip(:token_trivia)
    kind = peek.first

    if kind == :token_eoi ||
       kind == :token_rbracket ||
       !BINARY_OPERATORS.include?(kind) ||
       PRECEDENCES.fetch(kind, Precedence::LOWEST) < precedence
      break
    end

    left = parse_infix_expression(left)
  end

  left
end

#parse_filter_selectorObject



313
314
315
316
317
318
# File 'lib/json_p3/path/parser.rb', line 313

def parse_filter_selector
  token = eat(:token_question)
  expr = parse_filter_expression(Precedence::LOWEST)
  throw_for_not_compared(expr)
  FilterSelector.new(@env, token, FilterExpression.new(token, expr))
end

#parse_float_literalObject



530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/json_p3/path/parser.rb', line 530

def parse_float_literal
  token = self.next
  value = JSONP3::Path.get_token_value(token, @query)

  if value.start_with?("0") && value.split(".").first.length > 1
    raise SyntaxError.new(
      "invalid float literal",
      token,
      @query
    )
  end

  FloatLiteral.new(token, Float(value))
end

#parse_function_expressionObject



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/json_p3/path/parser.rb', line 340

def parse_function_expression
  token = eat(:token_name)
  eat(:token_lparen)

  args = [] #: Array[Expression]

  while peek.first != :token_rparen
    expr = parse_primary
    skip(:token_trivia)

    expr = parse_infix_expression(expr) while BINARY_OPERATORS.include?(peek.first)
    args << expr

    if peek.first != :token_rparen
      skip(:token_trivia)
      eat(:token_comma)
    end

  end

  skip(:token_trivia)
  eat(:token_rparen)

  name = JSONP3::Path.get_token_value(token, @query)
  func = @env.function_extensions[name]

  unless func
    raise JSONPathNameError.new(
      "unknown function extension #{name}",
      token,
      @query
    )
  end

  validate_function_signature(name, func, args, token)

  FunctionExpression.new(
    token,
    name,
    func,
    args
  )
end

#parse_grouped_expressionObject



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/json_p3/path/parser.rb', line 432

def parse_grouped_expression
  eat(:token_lparen)
  expr = parse_filter_expression(Precedence::LOWEST)

  loop do
    skip(:token_trivia)
    peeked = peek

    break if peeked.first == :token_rparen

    if peeked.first == :token_eoi
      raise SyntaxError.new(
        "unbalanced parentheses",
        peeked,
        @query
      )
    end

    expr = parse_infix_expression(expr)
  end

  skip(:token_trivia)
  eat(:token_rparen)
  expr
end

#parse_i_json_int(token) ⇒ Object



555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# File 'lib/json_p3/path/parser.rb', line 555

def parse_i_json_int(token)
  value = JSONP3::Path.get_token_value(token, @query)

  if value.length > 1 && value.start_with?("0", "-0")
    raise SyntaxError.new(
      "invalid index '#{value}'",
      token,
      @query
    )
  end

  begin
    int = Integer(value)
  rescue ArgumentError
    raise SyntaxError.new(
      "invalid I-JSON integer",
      token,
      @query
    )
  end

  if int < @env.class::MIN_INT_INDEX || int > @env.class::MAX_INT_INDEX
    raise SyntaxError.new(
      "index out of range",
      token,
      @query
    )
  end

  int
end

#parse_index_or_sliceObject



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/json_p3/path/parser.rb', line 265

def parse_index_or_slice
  token = self.next
  index = parse_i_json_int(token)
  skip(:token_trivia)

  return @env.class::INDEX_SELECTOR.new(@env, token, index) unless peek.first == :token_colon

  stop = nil
  step = nil

  eat(:token_colon)
  skip(:token_trivia)

  if peek.first == :token_index
    stop = parse_i_json_int(self.next)
    skip(:token_trivia)
  end

  if peek.first == :token_colon
    self.next
    skip(:token_trivia)
    step = parse_i_json_int(self.next) if peek.first == :token_index
  end

  SliceSelector.new(@env, token, index, stop, step)
end

#parse_infix_expression(left) ⇒ Object



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
# File 'lib/json_p3/path/parser.rb', line 466

def parse_infix_expression(left)
  token = self.next
  kind = token.first
  precedence = PRECEDENCES[kind] || Precedence::LOWEST
  right = parse_filter_expression(precedence)

  if COMPARISON_OPERATORS.include?(kind)
    throw_for_non_comparable(left)
    throw_for_non_comparable(right)

    case kind
    when :token_eq
      EqExpression.new(token, left, right)
    when :token_ne
      NeExpression.new(token, left, right)
    when :token_lt
      LtExpression.new(token, left, right)
    when :token_le
      LeExpression.new(token, left, right)
    when :token_gt
      GtExpression.new(token, left, right)
    when :token_ge
      GeExpression.new(token, left, right)
    else
      raise SyntaxError.new(
        "expected an infix operator",
        token,
        @query
      )
    end
  else
    throw_for_not_compared(left)
    throw_for_not_compared(right)

    case kind
    when :token_and
      LogicalAndExpression.new(token, left, right)
    when :token_or
      LogicalOrExpression.new(token, left, right)
    else
      raise SyntaxError.new(
        "expected an infix operator",
        token,
        @query
      )
    end
  end
end

#parse_integer_literalObject



515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'lib/json_p3/path/parser.rb', line 515

def parse_integer_literal
  token = self.next
  value = JSONP3::Path.get_token_value(token, @query)

  if value.start_with?("0") && value.length > 1
    raise SyntaxError.new(
      "invalid integer literal",
      token,
      @query
    )
  end

  IntegerLiteral.new(token, Integer(Float(value)))
end

#parse_prefix_expressionObject



458
459
460
461
462
463
464
# File 'lib/json_p3/path/parser.rb', line 458

def parse_prefix_expression
  token = eat(:token_not)
  LogicalNotExpression.new(
    token,
    parse_filter_expression(Precedence::PREFIX)
  )
end

#parse_primaryObject



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/json_p3/path/parser.rb', line 384

def parse_primary
  skip(:token_trivia)
  peeked = peek

  case peeked.first
  when :token_single_quoted_string, :token_double_quoted_string
    token = self.next
    StringLiteral.new(token, JSONP3::Path.get_token_value(token, @query))
  when :token_single_quoted_esc_string, :token_double_quoted_esc_string
    token = self.next
    StringLiteral.new(
      token,
      JSONP3::Path.unescape(
        JSONP3::Path.get_token_value(token, @query), token, @query
      )
    )
  when :token_name
    case JSONP3::Path.get_token_value(peeked, @query)
    when "null"
      NullLiteral.new(self.next, nil)
    when "false"
      BooleanLiteral.new(self.next, false)
    when "true"
      BooleanLiteral.new(self.next, true)
    else
      parse_function_expression
    end
  when :token_lparen
    parse_grouped_expression
  when :token_index, :token_int
    parse_integer_literal
  when :token_float
    parse_float_literal
  when :token_dollar
    parse_absolute_query
  when :token_at
    parse_relative_query
  when :token_not
    parse_prefix_expression
  else
    raise SyntaxError.new(
      "unexpected token #{peek.first}",
      self.next,
      @query
    )
  end
end

#parse_relative_queryObject



550
551
552
553
# File 'lib/json_p3/path/parser.rb', line 550

def parse_relative_query
  token = eat(:token_at)
  RelativeQueryExpression.new(token, Query.new(@env, parse_segments))
end

#parse_segmentsObject



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/json_p3/path/parser.rb', line 104

def parse_segments
  segments = [] #: Array[Segment]

  loop do
    case peek.first
    when :token_trivia
      @pos += 1
      if peek.first == :token_eoi
        raise SyntaxError.new(
          "unexpected trailing whitespace",
          @tokens[@pos - 1],
          @query
        )
      end
    when :token_double_dot
      segments << DescendantSegment.new(
        @env,
        self.next,
        parse_descendant_selectors
      )
    when :token_dot
      segments << ChildSegment.new(
        @env,
        self.next,
        [parse_shorthand_selector]
      )
    when :token_lbracket
      segments << ChildSegment.new(
        @env,
        peek,
        parse_bracketed_selectors
      )
    else
      break
    end
  end

  segments
end

#parse_shorthand_selectorObject



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/json_p3/path/parser.rb', line 159

def parse_shorthand_selector
  token = self.next

  case token.first
  when :token_name
    @env.class::NAME_SELECTOR.new(
      @env,
      token,
      JSONP3::Path.get_token_value(token, @query)
    )
  when :token_asterisk
    WildcardSelector.new(@env, token)
  else
    raise SyntaxError.new(
      "expected a shorthand selector",
      token,
      @query
    )
  end
end

#parse_slice_selectorObject



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/json_p3/path/parser.rb', line 292

def parse_slice_selector
  token = eat(:token_colon)
  skip(:token_trivia)

  stop = nil
  step = nil

  if peek.first == :token_index
    stop = parse_i_json_int(self.next)
    skip(:token_trivia)
  end

  if peek.first == :token_colon
    self.next
    skip(:token_trivia)
    step = parse_i_json_int(self.next) if peek.first == :token_index
  end

  SliceSelector.new(@env, token, nil, stop, step)
end

#peekObject



95
# File 'lib/json_p3/path/parser.rb', line 95

def peek = @tokens[@pos] || @eoi

#skip(kind) ⇒ Object



90
91
92
# File 'lib/json_p3/path/parser.rb', line 90

def skip(kind)
  @pos += 1 if (@tokens[@pos] || @eoi).first == kind
end

#throw_for_non_comparable(expression) ⇒ Object



606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/json_p3/path/parser.rb', line 606

def throw_for_non_comparable(expression)
  if expression.is_a?(QueryExpression) && !expression.query.singular?
    raise TypeError.new(
      "non-singular query is not comparable",
      expression.token,
      @query
    )
  end

  if expression.is_a?(FunctionExpression) &&
     expression.func.class::RETURN_TYPE != :value_expression
    raise TypeError.new(
      "result of #{expression.name}() is not comparable",
      expression.token,
      @query
    )
  end
end

#throw_for_not_compared(expression) ⇒ Object



587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
# File 'lib/json_p3/path/parser.rb', line 587

def throw_for_not_compared(expression)
  if expression.is_a?(FilterExpressionLiteral)
    raise TypeError.new(
      "filter expression literals must be compared",
      expression.token,
      @query
    )
  end

  if expression.is_a?(FunctionExpression) &&
     expression.func.class::RETURN_TYPE == :value_expression
    raise TypeError.new(
      "result of #{expression.name}() must be compared",
      expression.token,
      @query
    )
  end
end

#validate_function_signature(name, func, args, token) ⇒ Object



625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
# File 'lib/json_p3/path/parser.rb', line 625

def validate_function_signature(name, func, args, token)
  count = func.class::ARG_TYPES.length

  unless args.length == count
    raise TypeError.new(
      "#{name}() takes #{count} argument#{"s" unless count == 1} (#{args.length} given)",
      token,
      @query
    )
  end

  func.class::ARG_TYPES.each_with_index do |t, i|
    arg = args[i]
    case t
    when :value_expression
      unless arg.is_a?(FilterExpressionLiteral) ||
             (arg.is_a?(QueryExpression) && arg.query.singular?) ||
             (function_return_type(arg) == :value_expression)
        raise TypeError.new(
          "#{name}() argument #{i} must be of ValueType",
          arg.token,
          @query
        )
      end
    when :logical_expression
      unless arg.is_a?(QueryExpression) || arg.is_a?(InfixExpression)
        raise TypeError.new(
          "#{name}() argument #{i} must be of LogicalType",
          arg.token,
          @query
        )
      end
    when :nodes_expression
      unless arg.is_a?(QueryExpression) || function_return_type(arg) == :nodes_expression
        raise TypeError.new(
          "#{name}() argument #{i} must be of NodesType",
          arg.token,
          @query
        )
      end
    end
  end
end