janeway-jsonpath 0.6.0 → 1.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/janeway/ast/array_slice_selector.rb +6 -90
  4. data/lib/janeway/ast/binary_operator.rb +57 -51
  5. data/lib/janeway/ast/child_segment.rb +3 -5
  6. data/lib/janeway/ast/current_node.rb +2 -15
  7. data/lib/janeway/ast/descendant_segment.rb +0 -3
  8. data/lib/janeway/ast/expression.rb +35 -5
  9. data/lib/janeway/ast/function.rb +15 -10
  10. data/lib/janeway/ast/name_selector.rb +12 -7
  11. data/lib/janeway/ast/root_node.rb +2 -15
  12. data/lib/janeway/ast/selector.rb +5 -6
  13. data/lib/janeway/ast/unary_operator.rb +12 -2
  14. data/lib/janeway/ast/wildcard_selector.rb +2 -2
  15. data/lib/janeway/enumerator.rb +14 -7
  16. data/lib/janeway/error.rb +6 -1
  17. data/lib/janeway/functions/count.rb +1 -8
  18. data/lib/janeway/functions/length.rb +1 -12
  19. data/lib/janeway/functions/match.rb +3 -8
  20. data/lib/janeway/functions/search.rb +3 -8
  21. data/lib/janeway/functions/value.rb +1 -7
  22. data/lib/janeway/functions.rb +75 -2
  23. data/lib/janeway/interpreter.rb +50 -35
  24. data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
  25. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
  26. data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
  27. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
  28. data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
  29. data/lib/janeway/interpreters/function_interpreter.rb +5 -1
  30. data/lib/janeway/interpreters/index_selector_delete_if.rb +1 -0
  31. data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -1
  32. data/lib/janeway/interpreters/iteration_helper.rb +19 -5
  33. data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
  34. data/lib/janeway/interpreters/tree_constructor.rb +0 -20
  35. data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
  36. data/lib/janeway/interpreters/yielder.rb +8 -2
  37. data/lib/janeway/lexer.rb +66 -77
  38. data/lib/janeway/location.rb +3 -1
  39. data/lib/janeway/normalized_path.rb +23 -7
  40. data/lib/janeway/parser.rb +46 -34
  41. data/lib/janeway/query.rb +48 -5
  42. data/lib/janeway/token.rb +2 -5
  43. data/lib/janeway/version.rb +1 -1
  44. data/lib/janeway.rb +16 -3
  45. metadata +3 -10
  46. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
  47. data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
  48. data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
  49. data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
  50. data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
  51. data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
  52. data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
@@ -22,6 +22,10 @@ module Janeway
22
22
  def initialize(function)
23
23
  super
24
24
  @params = function.parameters.map { |param| TreeConstructor.ast_node_to_interpreter(param) }
25
+ # Build the function body from the registry. This is the point at
26
+ # which per-instance specialization happens — e.g. match/search
27
+ # compile a literal regex once, capturing it in the closure.
28
+ @body = Functions::REGISTRY.fetch(function.name)[:build].call(function.parameters)
25
29
  end
26
30
 
27
31
  # @param input [Array, Hash] the results of processing so far
@@ -30,7 +34,7 @@ module Janeway
30
34
  # @param _path [Array<String>] elements of normalized path to the current input
31
35
  def interpret(input, _parent, root, _path)
32
36
  params = interpret_function_parameters(@params, input, root)
33
- function.body.call(*params)
37
+ @body.call(*params)
34
38
  end
35
39
 
36
40
  # Evaluate the expressions in the parameter list to make the parameter values
@@ -28,6 +28,7 @@ module Janeway
28
28
 
29
29
  index = selector.value
30
30
  result = input.fetch(index) # raises IndexError if no such index
31
+ index += input.size if index.negative? # yield positive index for the normalize path
31
32
  return unless @yield_proc.call(input[index], input, path + [index])
32
33
 
33
34
  input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
@@ -31,7 +31,9 @@ module Janeway
31
31
  result = input.fetch(selector.value) # raises IndexError if no such index
32
32
  return [result] unless @next
33
33
 
34
- @next.interpret(result, input, root, path + [selector.value])
34
+ index = selector.value
35
+ index += input.size if index.negative?
36
+ @next.interpret(result, input, root, path + [index])
35
37
  rescue IndexError
36
38
  []
37
39
  end
@@ -6,21 +6,28 @@ module Janeway
6
6
  module Interpreters
7
7
  # Mixin for interpreter classes that yield to a block
8
8
  module IterationHelper
9
+ # Sentinel block used by Interpreter#make_deleter to turn a DeleteIf
10
+ # into an unconditional deleter. `equal?`-comparable so future fast
11
+ # paths (bulk clear) can detect it.
12
+ PASS_ALL = proc { true }.freeze
9
13
  # Returns a Proc that yields the correct number of parameters to a block
10
14
  #
11
15
  # block.arity is -1 when no block is given, and an enumerator is being returned
12
16
  # @return [Proc] which takes 3 parameters
13
17
  def make_yield_proc(&block)
18
+ # Bind the block locally so the returned proc captures a local, not the
19
+ # containing instance's @block ivar (avoids an ivar read per yield).
20
+ b = block
14
21
  if block.arity.negative?
15
22
  # Yield just the value to an enumerator, to enable instance method calls on
16
23
  # matched values like this: enum.delete_if(&:even?)
17
- proc { |value, _parent, _path| @block.call(value) }
24
+ proc { |value, _parent, _path| b.call(value) }
18
25
  elsif block.arity > 3
19
26
  # Only do the work of constructing the normalized path when it is actually used
20
- proc { |value, parent, path| @block.call(value, parent, path.last, normalized_path(path)) }
27
+ proc { |value, parent, path| b.call(value, parent, path.last, normalized_path(path)) }
21
28
  else
22
29
  # block arity is 1, 2 or 3. Send all 3 parameters regardless.
23
- proc { |value, parent, path| @block.call(value, parent, path.last) }
30
+ proc { |value, parent, path| b.call(value, parent, path.last) }
24
31
  end
25
32
  end
26
33
 
@@ -37,8 +44,15 @@ module Janeway
37
44
  def normalized_path(components)
38
45
  # First component is the root identifer, the remaining components are
39
46
  # all index selectors or name selectors.
40
- # Handle the root identifier separately, because .normalize does not handle those.
41
- '$' + components[1..].map { NormalizedPath.normalize(_1) }.join
47
+ # Build the result string in a single pass avoids `components[1..]`
48
+ # (slice allocation) plus `.map { ... }.join` (intermediate Array).
49
+ result = +'$'
50
+ i = 1
51
+ while i < components.size
52
+ result << NormalizedPath.normalize(components[i])
53
+ i += 1
54
+ end
55
+ result
42
56
  end
43
57
  end
44
58
  end
@@ -1,33 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'root_node_interpreter'
4
+ require_relative 'iteration_helper'
4
5
 
5
6
  module Janeway
6
7
  module Interpreters
7
- # Interprets the root node for deletion
8
- class RootNodeDeleter < RootNodeInterpreter
9
- # Delete all values from the root node.
10
- #
11
- # TODO: unclear, for deletion is there a difference between queries '$' and '$.*'?
8
+ # Delete values from the root node for which the block returns a truthy value.
9
+ class RootNodeDeleteIf < RootNodeInterpreter
10
+ include IterationHelper
11
+
12
+ # @param node [AST::RootNode]
13
+ def initialize(node, &block)
14
+ super(node)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Delete values from the root container for which the block returns truthy.
12
22
  #
13
23
  # @param _input [Array, Hash] the results of processing so far
14
24
  # @param _parent [Array, Hash] parent of the input object
15
25
  # @param root [Array, Hash] the entire input
16
26
  # @param _path [Array<String>] elements of normalized path to the current input
17
27
  # @return [Array] deleted elements
18
- def interpret(_input, _parent, root, _path)
28
+ def interpret(_input, _parent, root, _path = nil)
19
29
  case root
20
- when Array
21
- results = root.dup
22
- root.clear
23
- results
24
- when Hash
25
- results = root.values
26
- root.clear
27
- results
28
- else
29
- []
30
+ when Array then maybe_delete_array_values(root)
31
+ when Hash then maybe_delete_hash_values(root)
32
+ else []
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @param input [Array]
39
+ def maybe_delete_array_values(input)
40
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
41
+ if @block.equal?(IterationHelper::PASS_ALL)
42
+ results = input.dup
43
+ input.clear
44
+ return results
45
+ end
46
+
47
+ results = []
48
+ (input.size - 1).downto(0).each do |i|
49
+ next unless @yield_proc.call(input[i], input, ['$', i])
50
+
51
+ results << input.delete_at(i)
52
+ end
53
+ results.reverse
54
+ end
55
+
56
+ # @param input [Hash]
57
+ def maybe_delete_hash_values(input)
58
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
59
+ if @block.equal?(IterationHelper::PASS_ALL)
60
+ results = input.values
61
+ input.clear
62
+ return results
63
+ end
64
+
65
+ results = []
66
+ input.each do |key, value|
67
+ next unless @yield_proc.call(value, input, ['$', key])
68
+
69
+ results << input.delete(key)
30
70
  end
71
+ results
31
72
  end
32
73
  end
33
74
  end
@@ -44,26 +44,6 @@ module Janeway
44
44
  end
45
45
  end
46
46
 
47
- # Return a Deleter interpreter for the given AST node.
48
- # This interpreter deletes matched values.
49
- #
50
- # @param expr [AST::Expression]
51
- # @return [Interprteters::Base]
52
- def self.ast_node_to_deleter(expr)
53
- case expr
54
- when AST::IndexSelector then IndexSelectorDeleter.new(expr)
55
- when AST::ArraySliceSelector then ArraySliceSelectorDeleter.new(expr)
56
- when AST::NameSelector then NameSelectorDeleter.new(expr)
57
- when AST::FilterSelector then FilterSelectorDeleter.new(expr)
58
- when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
59
- when AST::ChildSegment then ChildSegmentDeleter.new(expr)
60
- when AST::RootNode then RootNodeDeleter.new(expr)
61
- when nil then nil # caller has no @next node
62
- else
63
- raise "Unknown AST expression: #{expr.inspect}"
64
- end
65
- end
66
-
67
47
  # Return a DeleteIf interpreter for the given AST node.
68
48
  # This interpreter deletes matched values, but only after
69
49
  # yielding to a block that returns a truthy value.
@@ -36,6 +36,15 @@ module Janeway
36
36
  # @param input [Array]
37
37
  # @param path [Array]
38
38
  def maybe_delete_array_values(input, path)
39
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
40
+ # Bulk clear is O(n) with much smaller constants than reverse-iterate
41
+ # + delete_at + per-element proc call.
42
+ if @block.equal?(IterationHelper::PASS_ALL)
43
+ results = input.dup
44
+ input.clear
45
+ return results
46
+ end
47
+
39
48
  results = []
40
49
  (input.size - 1).downto(0).each do |i|
41
50
  next unless @yield_proc.yield(input[i], input, path + [i])
@@ -48,6 +57,13 @@ module Janeway
48
57
  # @param input [Hash]
49
58
  # @param path [Array]
50
59
  def maybe_delete_hash_values(input, path)
60
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
61
+ if @block.equal?(IterationHelper::PASS_ALL)
62
+ results = input.values
63
+ input.clear
64
+ return results
65
+ end
66
+
51
67
  results = []
52
68
  input.each do |key, value|
53
69
  next unless @yield_proc.yield(value, input, path + [key])
@@ -19,6 +19,12 @@ module Janeway
19
19
  @yield_proc = make_yield_proc(&block)
20
20
  end
21
21
 
22
+ # Frozen empty array — Yielder is only used by Enumerator#each and
23
+ # #find_paths, both of which discard the traversal's return value
24
+ # (results are surfaced via yield instead). Returning a shared frozen
25
+ # array avoids allocating a wrapper array per leaf.
26
+ EMPTY_RESULT = [].freeze
27
+
22
28
  # Yield each input value
23
29
  #
24
30
  # @param input [Array, Hash] the results of processing so far
@@ -26,10 +32,10 @@ module Janeway
26
32
  # @param _root [Array, Hash] the entire input
27
33
  # @param path [Array<String, Integer>] components of normalized path to the current input
28
34
  # @yieldparam [Object] matched value
29
- # @return [Object] input as node list
35
+ # @return [Array] frozen empty array — see EMPTY_RESULT
30
36
  def interpret(input, parent, _root, path)
31
37
  @yield_proc.call(input, parent, path)
32
- input.is_a?(Array) ? input : [input]
38
+ EMPTY_RESULT
33
39
  end
34
40
 
35
41
  # Dummy method from Interpreters::Base, allow child segment interpreter to disable the
data/lib/janeway/lexer.rb CHANGED
@@ -38,9 +38,23 @@ module Janeway
38
38
  TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
39
39
  ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
40
40
 
41
+ # O(1)-lookup companions to the arrays above. Hash#include? and the reverse
42
+ # lookup avoid the linear scan that .include? / .key would do per token.
43
+ LEXEME_TO_TYPE = OPERATORS.invert.freeze
44
+ ONE_CHAR_LEX_SET = ONE_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
45
+ TWO_CHAR_LEX_SET = TWO_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
46
+ TWO_CHAR_LEX_FIRST_SET = TWO_CHAR_LEX_FIRST.each_with_object({}) { |c, h| h[c] = true }.freeze
47
+ ONE_OR_TWO_CHAR_LEX_SET = ONE_OR_TWO_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
48
+
41
49
  WHITESPACE = " \t\n\r"
42
50
  KEYWORD = %w[true false null].freeze
51
+ KEYWORD_SET = KEYWORD.each_with_object({}) { |k, h| h[k] = true }.freeze
43
52
  FUNCTIONS = %w[length count match search value].freeze
53
+ FUNCTIONS_SET = FUNCTIONS.each_with_object({}) { |k, h| h[k] = true }.freeze
54
+
55
+ # Hoisted from lex_delimited_string. Given a delimiter, the value is the "other" delimiter.
56
+ OTHER_DELIMITER = { "'" => '"', '"' => "'" }.freeze
57
+ QUOTE_LEX_SET = { "'" => true, '"' => true }.freeze
44
58
 
45
59
  # faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
46
60
  ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
@@ -72,6 +86,7 @@ module Janeway
72
86
  end
73
87
 
74
88
  def start_tokenization
89
+ raise err('JSONPath query is empty') if @source.empty?
75
90
  if WHITESPACE.include?(@source[0]) || WHITESPACE.include?(@source[-1])
76
91
  raise err('JSONPath query may not start or end with whitespace')
77
92
  end
@@ -88,18 +103,18 @@ module Janeway
88
103
  return if WHITESPACE.include?(c)
89
104
 
90
105
  token =
91
- if ONE_OR_TWO_CHAR_LEX.include?(c)
106
+ if ONE_OR_TWO_CHAR_LEX_SET.include?(c)
92
107
  token_from_one_or_two_char_lex(c)
93
- elsif ONE_CHAR_LEX.include?(c)
108
+ elsif ONE_CHAR_LEX_SET.include?(c)
94
109
  token_from_one_char_lex(c)
95
- elsif TWO_CHAR_LEX_FIRST.include?(c)
110
+ elsif TWO_CHAR_LEX_FIRST_SET.include?(c)
96
111
  token_from_two_char_lex(c)
97
- elsif %w[" '].include?(c)
112
+ elsif QUOTE_LEX_SET.include?(c)
98
113
  lex_delimited_string(c)
99
114
  elsif digit?(c)
100
115
  lex_number
101
116
  elsif name_first_char?(c)
102
- lex_member_name_shorthand(ignore_keywords: tokens.last&.type == :dot)
117
+ lex_member_name_shorthand(ignore_keywords: previous_token_type == :dot)
103
118
  end
104
119
  raise err("Unknown character: #{c.inspect}") unless token
105
120
 
@@ -126,20 +141,20 @@ module Janeway
126
141
  raise err("Operator #{lexeme.inspect} must not be followed by whitespace")
127
142
  end
128
143
 
129
- Token.new(OPERATORS.key(lexeme), lexeme, nil, current_location)
144
+ Token.new(LEXEME_TO_TYPE[lexeme], lexeme, nil, current_location)
130
145
  end
131
146
 
132
147
  # Consumes an operator that could be either 1 or 2 chars in length
133
148
  # @return [Token]
134
149
  def token_from_one_or_two_char_lex(lexeme)
135
- next_two_chars = [lexeme, lookahead].join
136
- if TWO_CHAR_LEX.include?(next_two_chars)
150
+ next_two_chars = lexeme + lookahead
151
+ if TWO_CHAR_LEX_SET.include?(next_two_chars)
137
152
  consume
138
153
  if next_two_chars == '..' && WHITESPACE.include?(lookahead)
139
154
  raise err("Operator #{next_two_chars.inspect} must not be followed by whitespace")
140
155
  end
141
156
 
142
- Token.new(OPERATORS.key(next_two_chars), next_two_chars, nil, current_location)
157
+ Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
143
158
  else
144
159
  token_from_one_char_lex(lexeme)
145
160
  end
@@ -148,11 +163,11 @@ module Janeway
148
163
  # Consumes a 2 char operator
149
164
  # @return [Token]
150
165
  def token_from_two_char_lex(lexeme)
151
- next_two_chars = [lexeme, lookahead].join
152
- raise err("Unknown operator \"#{lexeme}\"") unless TWO_CHAR_LEX.include?(next_two_chars)
166
+ next_two_chars = lexeme + lookahead
167
+ raise err("Unknown operator \"#{lexeme}\"") unless TWO_CHAR_LEX_SET.include?(next_two_chars)
153
168
 
154
169
  consume
155
- Token.new(OPERATORS.key(next_two_chars), next_two_chars, nil, current_location)
170
+ Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
156
171
  end
157
172
 
158
173
  def consume
@@ -165,12 +180,24 @@ module Janeway
165
180
  consume while digit?(lookahead)
166
181
  end
167
182
 
183
+ # @return [String] source substring for the lexeme currently being built
184
+ def current_lexeme
185
+ source[lexeme_start_p..(next_p - 1)]
186
+ end
187
+
188
+ # Type of the most-recently-emitted token, or nil if none.
189
+ # Named lookback for the two context-sensitive lexing decisions
190
+ # (identifier-in-brackets rejection; keyword ignore after `.`).
191
+ # @return [Symbol, nil]
192
+ def previous_token_type
193
+ @tokens.last&.type
194
+ end
195
+
168
196
  # @param delimiter [String] eg. ' or "
169
197
  # @return [Token] string token
170
198
  def lex_delimited_string(delimiter)
171
- allowed_delimiters = %w[' "]
172
199
  # the "other" delimiter char, which is not currently being treated as a delimiter
173
- non_delimiter = allowed_delimiters.reject { |char| char == delimiter }.first
200
+ non_delimiter = OTHER_DELIMITER[delimiter]
174
201
 
175
202
  literal_chars = []
176
203
  while lookahead != delimiter && source_uncompleted?
@@ -189,7 +216,7 @@ module Janeway
189
216
  end
190
217
  elsif unescaped?(next_char)
191
218
  consume
192
- elsif allowed_delimiters.include?(next_char) && next_char != delimiter
219
+ elsif QUOTE_LEX_SET.include?(next_char) && next_char != delimiter
193
220
  consume
194
221
  else
195
222
  raise err("invalid character #{next_char.inspect}")
@@ -203,7 +230,7 @@ module Janeway
203
230
  literal = literal_chars.join
204
231
 
205
232
  # lexeme value includes delimiters and literal escape characters
206
- lexeme = source[lexeme_start_p..(next_p - 1)]
233
+ lexeme = current_lexeme
207
234
 
208
235
  Token.new(:string, lexeme, literal, current_location)
209
236
  end
@@ -284,10 +311,11 @@ module Janeway
284
311
  # @param low_surrogate_hex [String] string of hex digits, eg. "DE09"
285
312
  # @return [String] UTF-8 string containing a single multi-byte unicode character, eg. "😉"
286
313
  def convert_surrogate_pair_to_codepoint(high_surrogate_hex, low_surrogate_hex)
287
- [high_surrogate_hex, low_surrogate_hex].each do |hex_str|
288
- raise ArgumentError, "expect 4 hex digits, got #{hex_string.inspect}" unless hex_str.size == 4
289
- end
290
-
314
+ # Callers guarantee 4-char inputs (consume_four_hex_digits followed by
315
+ # high_surrogate?/low_surrogate? which pre-check size). The old defensive
316
+ # guard here referenced a non-existent `hex_string` local — dead code
317
+ # that would NameError if ever reached.
318
+ #
291
319
  # Calculate the code point from the surrogate pair values
292
320
  # algorithm from https://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm
293
321
  high = high_surrogate_hex.hex
@@ -316,20 +344,19 @@ module Janeway
316
344
  #
317
345
  # @return [String]
318
346
  def consume_four_hex_digits
319
- hex_digits = []
347
+ hex_digits = String.new(capacity: 4)
320
348
  4.times do
321
- hex_digits << consume
322
- case hex_digits.last.ord
323
- when 0x30..0x39 then next # '0'..'1'
324
- when 0x40..0x46 then next # 'A'..'F'
349
+ c = consume
350
+ hex_digits << c
351
+ case c.ord
352
+ when 0x30..0x39 then next # '0'..'9'
353
+ when 0x41..0x46 then next # 'A'..'F'
325
354
  when 0x61..0x66 then next # 'a'..'f'
326
355
  else
327
- raise err("Invalid unicode escape sequence: \\u#{hex_digits.join}")
356
+ raise err("Invalid unicode escape sequence: \\u#{hex_digits}")
328
357
  end
329
358
  end
330
- raise err("Incomplete unicode escape sequence: \\u#{hex_digits.join}") if hex_digits.size < 4
331
-
332
- hex_digits.join
359
+ hex_digits
333
360
  end
334
361
 
335
362
  # Consume a numeric string. May be an integer, fractional, or exponent.
@@ -352,13 +379,13 @@ module Janeway
352
379
  consume # "+" / "-"
353
380
  end
354
381
  unless digit?(lookahead)
355
- lexeme = source[lexeme_start_p..(next_p - 1)]
382
+ lexeme = current_lexeme
356
383
  raise err("Exponent 'e' must be followed by number: #{lexeme.inspect}")
357
384
  end
358
385
  consume_digits
359
386
  end
360
387
 
361
- lexeme = source[lexeme_start_p..(next_p - 1)]
388
+ lexeme = current_lexeme
362
389
  if lexeme.start_with?('0') && lexeme.size > 1
363
390
  raise err("Number may not start with leading zero: #{lexeme.inspect}")
364
391
  end
@@ -374,36 +401,6 @@ module Janeway
374
401
 
375
402
  # Consume an alphanumeric string.
376
403
  # If `ignore_keywords`, the result is always an :identifier token.
377
- # Otherwise, keywords and function names will be recognized and tokenized as those types.
378
- #
379
- # @param ignore_keywords [Boolean]
380
- def lex_identifier(ignore_keywords: false)
381
- consume while alpha_numeric?(lookahead)
382
-
383
- identifier = source[lexeme_start_p..(next_p - 1)]
384
- type =
385
- if KEYWORD.include?(identifier) && !ignore_keywords
386
- identifier.to_sym
387
- elsif FUNCTIONS.include?(identifier) && !ignore_keywords
388
- :function
389
- else
390
- :identifier
391
- end
392
-
393
- Token.new(type, identifier, identifier, current_location)
394
- end
395
-
396
- # Parse an identifier string which is not within delimiters.
397
- # The standard set of unicode code points are allowed.
398
- # No character escapes are allowed.
399
- # Keywords and function names are ignored in this context.
400
- # @return [Token]
401
- def lex_unescaped_identifier
402
- consume while unescaped?(lookahead)
403
- identifier = source[lexeme_start_p..(next_p - 1)]
404
- Token.new(:identifier, identifier, identifier, current_location)
405
- end
406
-
407
404
  # Return true if string matches the definition of "unescaped" from RFC9535:
408
405
  # unescaped = %x20-21 / ; see RFC 8259
409
406
  # ; omit 0x22 "
@@ -427,19 +424,6 @@ module Janeway
427
424
  end
428
425
  end
429
426
 
430
- def escapable?(char)
431
- case char.ord
432
- when 0x62 then true # backspace
433
- when 0x66 then true # form feed
434
- when 0x6E then true # line feed
435
- when 0x72 then true # carriage return
436
- when 0x74 then true # horizontal tab
437
- when 0x2F then true # slash
438
- when 0x5C then true # backslash
439
- else false
440
- end
441
- end
442
-
443
427
  # True if character is suitable as the first character in a name selector
444
428
  # using shorthand notation (ie. no bracket notation.)
445
429
  #
@@ -470,6 +454,7 @@ module Janeway
470
454
  end
471
455
 
472
456
  # Lex a member name that is found within dot notation.
457
+ # This name is not delimited and allows a subset of the characters that can appear in a delimited string.
473
458
  #
474
459
  # Recognize keywords and given them the correct type.
475
460
  # @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.5.1.1-3
@@ -477,12 +462,16 @@ module Janeway
477
462
  # @param ignore_keywords [Boolean]
478
463
  # @return [Token]
479
464
  def lex_member_name_shorthand(ignore_keywords: false)
465
+ # Abort if name is preceded by child_start. Catches non-delimited identifiers in brackets,
466
+ # eg. $["key"] is allowed, but $[key] is not
467
+ raise err('Identifier within brackets must be surrounded by quotes') if previous_token_type == :child_start
468
+
480
469
  consume while name_char?(lookahead)
481
- identifier = source[lexeme_start_p..(next_p - 1)]
470
+ identifier = current_lexeme
482
471
  type =
483
- if KEYWORD.include?(identifier) && !ignore_keywords
472
+ if KEYWORD_SET.include?(identifier) && !ignore_keywords
484
473
  identifier.to_sym
485
- elsif FUNCTIONS.include?(identifier) && !ignore_keywords
474
+ elsif FUNCTIONS_SET.include?(identifier) && !ignore_keywords
486
475
  :function
487
476
  else
488
477
  :identifier
@@ -1,3 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Location = Struct.new(:col, :length)
3
+ module Janeway
4
+ Location = Struct.new(:col, :length)
5
+ end
@@ -11,6 +11,15 @@ module Janeway
11
11
  # Characters that do not need escaping, defined by hexadecimal range
12
12
  NORMAL_UNESCAPED_RANGES = [(0x20..0x26), (0x28..0x5B), (0x5D..0xD7FF), (0xE000..0x10FFFF)].freeze
13
13
 
14
+ # Fast-path check for `escape`: a single anchored regex covering the same
15
+ # ranges as NORMAL_UNESCAPED_RANGES. Avoids allocating a `chars` array and
16
+ # scanning four Range objects per character on the common all-normal case.
17
+ NORMAL_UNESCAPED_REGEX = Regexp.new(
18
+ '\A[' +
19
+ NORMAL_UNESCAPED_RANGES.map { |r| "\\u{#{r.min.to_s(16)}}-\\u{#{r.max.to_s(16)}}" }.join +
20
+ ']*\z'
21
+ ).freeze
22
+
14
23
  def self.normalize(value)
15
24
  case value
16
25
  when String then normalize_name(value)
@@ -33,8 +42,8 @@ module Janeway
33
42
  end
34
43
 
35
44
  def self.escape(str)
36
- # Common case, all chars are normal.
37
- return str if str.chars.all? { |char| NORMAL_UNESCAPED_RANGES.any? { |range| range.include?(char.ord) } }
45
+ # Common case, all chars are normal — one regex match, no allocations.
46
+ return str if NORMAL_UNESCAPED_REGEX.match?(str)
38
47
 
39
48
  # Some escaping must be done
40
49
  str.chars.map { |char| escape_char(char) }.join
@@ -46,11 +55,18 @@ module Janeway
46
55
  def self.escape_char(char)
47
56
  # Character ranges defined by https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7-8
48
57
  case char.ord
49
- when 0x20..0x26, 0x28..0x5B, 0x5D..0xD7FF, 0xE000..0x10FFFF # normal-unescaped range
50
- char # unescaped
51
- when 0x62, 0x66, 0x6E, 0x72, 0x74, 0x27, 0x5C # normal-escapable range
52
- # backspace, form feed, line feed, carriage return, horizontal tab, apostrophe, backslash
53
- "\\#{char}" # escaped
58
+ # normal-unescaped range
59
+ when 0x20..0x26, 0x28..0x5B, 0x5D..0xD7FF, 0xE000..0x10FFFF then char # unescaped
60
+
61
+ # normal-escapable range
62
+ when 0x08 then '\\b' # backspace
63
+ when 0x0C then '\\f' # form feed
64
+ when 0x0A then '\\n' # line feed / newline
65
+ when 0x0D then '\\r' # carriage return
66
+ when 0x09 then '\\t' # horizontal tab
67
+ when 0x27 then '\\\'' # apostrophe
68
+ when 0x5C then '\\\\' # backslash
69
+
54
70
  else # normal-hexchar range
55
71
  hex_encode_char(char)
56
72
  end