janeway-jsonpath 1.0.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.
- checksums.yaml +4 -4
- data/lib/janeway/ast/array_slice_selector.rb +6 -90
- data/lib/janeway/ast/binary_operator.rb +57 -51
- data/lib/janeway/ast/child_segment.rb +2 -5
- data/lib/janeway/ast/current_node.rb +2 -15
- data/lib/janeway/ast/descendant_segment.rb +0 -3
- data/lib/janeway/ast/expression.rb +34 -5
- data/lib/janeway/ast/function.rb +15 -10
- data/lib/janeway/ast/name_selector.rb +12 -7
- data/lib/janeway/ast/root_node.rb +2 -15
- data/lib/janeway/ast/selector.rb +5 -6
- data/lib/janeway/ast/unary_operator.rb +12 -2
- data/lib/janeway/ast/wildcard_selector.rb +2 -2
- data/lib/janeway/enumerator.rb +2 -4
- data/lib/janeway/error.rb +6 -1
- data/lib/janeway/functions/count.rb +1 -8
- data/lib/janeway/functions/length.rb +1 -12
- data/lib/janeway/functions/match.rb +3 -8
- data/lib/janeway/functions/search.rb +3 -8
- data/lib/janeway/functions/value.rb +1 -7
- data/lib/janeway/functions.rb +75 -2
- data/lib/janeway/interpreter.rb +50 -31
- data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
- data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
- data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
- data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
- data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
- data/lib/janeway/interpreters/function_interpreter.rb +5 -1
- data/lib/janeway/interpreters/index_selector_delete_if.rb +0 -1
- data/lib/janeway/interpreters/iteration_helper.rb +19 -5
- data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
- data/lib/janeway/interpreters/tree_constructor.rb +0 -20
- data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
- data/lib/janeway/interpreters/yielder.rb +8 -2
- data/lib/janeway/lexer.rb +61 -78
- data/lib/janeway/location.rb +3 -1
- data/lib/janeway/normalized_path.rb +11 -2
- data/lib/janeway/parser.rb +46 -34
- data/lib/janeway/query.rb +47 -2
- data/lib/janeway/token.rb +2 -5
- data/lib/janeway/version.rb +1 -1
- metadata +2 -9
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
- data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
- data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
- data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
- data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
- data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
- data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
|
@@ -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|
|
|
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|
|
|
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|
|
|
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
|
-
#
|
|
41
|
-
|
|
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
|
-
#
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 [
|
|
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
|
-
|
|
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'
|
|
@@ -89,18 +103,18 @@ module Janeway
|
|
|
89
103
|
return if WHITESPACE.include?(c)
|
|
90
104
|
|
|
91
105
|
token =
|
|
92
|
-
if
|
|
106
|
+
if ONE_OR_TWO_CHAR_LEX_SET.include?(c)
|
|
93
107
|
token_from_one_or_two_char_lex(c)
|
|
94
|
-
elsif
|
|
108
|
+
elsif ONE_CHAR_LEX_SET.include?(c)
|
|
95
109
|
token_from_one_char_lex(c)
|
|
96
|
-
elsif
|
|
110
|
+
elsif TWO_CHAR_LEX_FIRST_SET.include?(c)
|
|
97
111
|
token_from_two_char_lex(c)
|
|
98
|
-
elsif
|
|
112
|
+
elsif QUOTE_LEX_SET.include?(c)
|
|
99
113
|
lex_delimited_string(c)
|
|
100
114
|
elsif digit?(c)
|
|
101
115
|
lex_number
|
|
102
116
|
elsif name_first_char?(c)
|
|
103
|
-
lex_member_name_shorthand(ignore_keywords:
|
|
117
|
+
lex_member_name_shorthand(ignore_keywords: previous_token_type == :dot)
|
|
104
118
|
end
|
|
105
119
|
raise err("Unknown character: #{c.inspect}") unless token
|
|
106
120
|
|
|
@@ -127,20 +141,20 @@ module Janeway
|
|
|
127
141
|
raise err("Operator #{lexeme.inspect} must not be followed by whitespace")
|
|
128
142
|
end
|
|
129
143
|
|
|
130
|
-
Token.new(
|
|
144
|
+
Token.new(LEXEME_TO_TYPE[lexeme], lexeme, nil, current_location)
|
|
131
145
|
end
|
|
132
146
|
|
|
133
147
|
# Consumes an operator that could be either 1 or 2 chars in length
|
|
134
148
|
# @return [Token]
|
|
135
149
|
def token_from_one_or_two_char_lex(lexeme)
|
|
136
|
-
next_two_chars =
|
|
137
|
-
if
|
|
150
|
+
next_two_chars = lexeme + lookahead
|
|
151
|
+
if TWO_CHAR_LEX_SET.include?(next_two_chars)
|
|
138
152
|
consume
|
|
139
153
|
if next_two_chars == '..' && WHITESPACE.include?(lookahead)
|
|
140
154
|
raise err("Operator #{next_two_chars.inspect} must not be followed by whitespace")
|
|
141
155
|
end
|
|
142
156
|
|
|
143
|
-
Token.new(
|
|
157
|
+
Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
|
|
144
158
|
else
|
|
145
159
|
token_from_one_char_lex(lexeme)
|
|
146
160
|
end
|
|
@@ -149,11 +163,11 @@ module Janeway
|
|
|
149
163
|
# Consumes a 2 char operator
|
|
150
164
|
# @return [Token]
|
|
151
165
|
def token_from_two_char_lex(lexeme)
|
|
152
|
-
next_two_chars =
|
|
153
|
-
raise err("Unknown operator \"#{lexeme}\"") unless
|
|
166
|
+
next_two_chars = lexeme + lookahead
|
|
167
|
+
raise err("Unknown operator \"#{lexeme}\"") unless TWO_CHAR_LEX_SET.include?(next_two_chars)
|
|
154
168
|
|
|
155
169
|
consume
|
|
156
|
-
Token.new(
|
|
170
|
+
Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
|
|
157
171
|
end
|
|
158
172
|
|
|
159
173
|
def consume
|
|
@@ -166,12 +180,24 @@ module Janeway
|
|
|
166
180
|
consume while digit?(lookahead)
|
|
167
181
|
end
|
|
168
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
|
+
|
|
169
196
|
# @param delimiter [String] eg. ' or "
|
|
170
197
|
# @return [Token] string token
|
|
171
198
|
def lex_delimited_string(delimiter)
|
|
172
|
-
allowed_delimiters = %w[' "]
|
|
173
199
|
# the "other" delimiter char, which is not currently being treated as a delimiter
|
|
174
|
-
non_delimiter =
|
|
200
|
+
non_delimiter = OTHER_DELIMITER[delimiter]
|
|
175
201
|
|
|
176
202
|
literal_chars = []
|
|
177
203
|
while lookahead != delimiter && source_uncompleted?
|
|
@@ -190,7 +216,7 @@ module Janeway
|
|
|
190
216
|
end
|
|
191
217
|
elsif unescaped?(next_char)
|
|
192
218
|
consume
|
|
193
|
-
elsif
|
|
219
|
+
elsif QUOTE_LEX_SET.include?(next_char) && next_char != delimiter
|
|
194
220
|
consume
|
|
195
221
|
else
|
|
196
222
|
raise err("invalid character #{next_char.inspect}")
|
|
@@ -204,7 +230,7 @@ module Janeway
|
|
|
204
230
|
literal = literal_chars.join
|
|
205
231
|
|
|
206
232
|
# lexeme value includes delimiters and literal escape characters
|
|
207
|
-
lexeme =
|
|
233
|
+
lexeme = current_lexeme
|
|
208
234
|
|
|
209
235
|
Token.new(:string, lexeme, literal, current_location)
|
|
210
236
|
end
|
|
@@ -285,10 +311,11 @@ module Janeway
|
|
|
285
311
|
# @param low_surrogate_hex [String] string of hex digits, eg. "DE09"
|
|
286
312
|
# @return [String] UTF-8 string containing a single multi-byte unicode character, eg. "😉"
|
|
287
313
|
def convert_surrogate_pair_to_codepoint(high_surrogate_hex, low_surrogate_hex)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
#
|
|
292
319
|
# Calculate the code point from the surrogate pair values
|
|
293
320
|
# algorithm from https://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm
|
|
294
321
|
high = high_surrogate_hex.hex
|
|
@@ -317,20 +344,19 @@ module Janeway
|
|
|
317
344
|
#
|
|
318
345
|
# @return [String]
|
|
319
346
|
def consume_four_hex_digits
|
|
320
|
-
hex_digits =
|
|
347
|
+
hex_digits = String.new(capacity: 4)
|
|
321
348
|
4.times do
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
when
|
|
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'
|
|
326
354
|
when 0x61..0x66 then next # 'a'..'f'
|
|
327
355
|
else
|
|
328
|
-
raise err("Invalid unicode escape sequence: \\u#{hex_digits
|
|
356
|
+
raise err("Invalid unicode escape sequence: \\u#{hex_digits}")
|
|
329
357
|
end
|
|
330
358
|
end
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
hex_digits.join
|
|
359
|
+
hex_digits
|
|
334
360
|
end
|
|
335
361
|
|
|
336
362
|
# Consume a numeric string. May be an integer, fractional, or exponent.
|
|
@@ -353,13 +379,13 @@ module Janeway
|
|
|
353
379
|
consume # "+" / "-"
|
|
354
380
|
end
|
|
355
381
|
unless digit?(lookahead)
|
|
356
|
-
lexeme =
|
|
382
|
+
lexeme = current_lexeme
|
|
357
383
|
raise err("Exponent 'e' must be followed by number: #{lexeme.inspect}")
|
|
358
384
|
end
|
|
359
385
|
consume_digits
|
|
360
386
|
end
|
|
361
387
|
|
|
362
|
-
lexeme =
|
|
388
|
+
lexeme = current_lexeme
|
|
363
389
|
if lexeme.start_with?('0') && lexeme.size > 1
|
|
364
390
|
raise err("Number may not start with leading zero: #{lexeme.inspect}")
|
|
365
391
|
end
|
|
@@ -375,36 +401,6 @@ module Janeway
|
|
|
375
401
|
|
|
376
402
|
# Consume an alphanumeric string.
|
|
377
403
|
# If `ignore_keywords`, the result is always an :identifier token.
|
|
378
|
-
# Otherwise, keywords and function names will be recognized and tokenized as those types.
|
|
379
|
-
#
|
|
380
|
-
# @param ignore_keywords [Boolean]
|
|
381
|
-
def lex_identifier(ignore_keywords: false)
|
|
382
|
-
consume while alpha_numeric?(lookahead)
|
|
383
|
-
|
|
384
|
-
identifier = source[lexeme_start_p..(next_p - 1)]
|
|
385
|
-
type =
|
|
386
|
-
if KEYWORD.include?(identifier) && !ignore_keywords
|
|
387
|
-
identifier.to_sym
|
|
388
|
-
elsif FUNCTIONS.include?(identifier) && !ignore_keywords
|
|
389
|
-
:function
|
|
390
|
-
else
|
|
391
|
-
:identifier
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
Token.new(type, identifier, identifier, current_location)
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
# Parse an identifier string which is not within delimiters.
|
|
398
|
-
# The standard set of unicode code points are allowed.
|
|
399
|
-
# No character escapes are allowed.
|
|
400
|
-
# Keywords and function names are ignored in this context.
|
|
401
|
-
# @return [Token]
|
|
402
|
-
def lex_unescaped_identifier
|
|
403
|
-
consume while unescaped?(lookahead)
|
|
404
|
-
identifier = source[lexeme_start_p..(next_p - 1)]
|
|
405
|
-
Token.new(:identifier, identifier, identifier, current_location)
|
|
406
|
-
end
|
|
407
|
-
|
|
408
404
|
# Return true if string matches the definition of "unescaped" from RFC9535:
|
|
409
405
|
# unescaped = %x20-21 / ; see RFC 8259
|
|
410
406
|
# ; omit 0x22 "
|
|
@@ -428,19 +424,6 @@ module Janeway
|
|
|
428
424
|
end
|
|
429
425
|
end
|
|
430
426
|
|
|
431
|
-
def escapable?(char)
|
|
432
|
-
case char.ord
|
|
433
|
-
when 0x62 then true # backspace
|
|
434
|
-
when 0x66 then true # form feed
|
|
435
|
-
when 0x6E then true # line feed
|
|
436
|
-
when 0x72 then true # carriage return
|
|
437
|
-
when 0x74 then true # horizontal tab
|
|
438
|
-
when 0x2F then true # slash
|
|
439
|
-
when 0x5C then true # backslash
|
|
440
|
-
else false
|
|
441
|
-
end
|
|
442
|
-
end
|
|
443
|
-
|
|
444
427
|
# True if character is suitable as the first character in a name selector
|
|
445
428
|
# using shorthand notation (ie. no bracket notation.)
|
|
446
429
|
#
|
|
@@ -481,14 +464,14 @@ module Janeway
|
|
|
481
464
|
def lex_member_name_shorthand(ignore_keywords: false)
|
|
482
465
|
# Abort if name is preceded by child_start. Catches non-delimited identifiers in brackets,
|
|
483
466
|
# eg. $["key"] is allowed, but $[key] is not
|
|
484
|
-
raise err('Identifier within brackets must be surrounded by quotes') if
|
|
467
|
+
raise err('Identifier within brackets must be surrounded by quotes') if previous_token_type == :child_start
|
|
485
468
|
|
|
486
469
|
consume while name_char?(lookahead)
|
|
487
|
-
identifier =
|
|
470
|
+
identifier = current_lexeme
|
|
488
471
|
type =
|
|
489
|
-
if
|
|
472
|
+
if KEYWORD_SET.include?(identifier) && !ignore_keywords
|
|
490
473
|
identifier.to_sym
|
|
491
|
-
elsif
|
|
474
|
+
elsif FUNCTIONS_SET.include?(identifier) && !ignore_keywords
|
|
492
475
|
:function
|
|
493
476
|
else
|
|
494
477
|
:identifier
|
data/lib/janeway/location.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/janeway/parser.rb
CHANGED
|
@@ -16,6 +16,32 @@ module Janeway
|
|
|
16
16
|
UNARY_OPERATORS = %w[! -].freeze
|
|
17
17
|
BINARY_OPERATORS = %w[== != > < >= <= ,].freeze
|
|
18
18
|
LOGICAL_OPERATORS = %w[&& ||].freeze
|
|
19
|
+
|
|
20
|
+
# O(1)-lookup Hash companions to the arrays above, plus the union used by
|
|
21
|
+
# determine_infix_function. Building these inline in the hot path
|
|
22
|
+
# allocated a fresh Array per token; hoisting eliminates it.
|
|
23
|
+
UNARY_OPERATOR_SET = UNARY_OPERATORS.each_with_object({}) { |op, h| h[op] = true }.freeze
|
|
24
|
+
BINARY_OPERATOR_SET = BINARY_OPERATORS.each_with_object({}) { |op, h| h[op] = true }.freeze
|
|
25
|
+
INFIX_OPERATOR_SET =
|
|
26
|
+
(BINARY_OPERATORS + LOGICAL_OPERATORS).each_with_object({}) { |op, h| h[op] = true }.freeze
|
|
27
|
+
PARSE_METHOD_TYPES =
|
|
28
|
+
%I[identifier number string true false nil function if while root current_node]
|
|
29
|
+
.each_with_object({}) { |t, h| h[t] = true }.freeze
|
|
30
|
+
TERMINATOR_TYPES = %I[child_end union eof].each_with_object({}) { |t, h| h[t] = true }.freeze
|
|
31
|
+
|
|
32
|
+
# O(1) dispatch table for determine_parsing_function's type-driven cases.
|
|
33
|
+
# Sits next to the PARSE_METHOD_TYPES set (the other type branch) so both
|
|
34
|
+
# are visible when adding a new token type.
|
|
35
|
+
PARSE_DISPATCH_BY_TYPE = {
|
|
36
|
+
group_start: :parse_grouped_expr,
|
|
37
|
+
:"\n" => :parse_terminator,
|
|
38
|
+
eof: :parse_terminator,
|
|
39
|
+
child_start: :parse_child_segment,
|
|
40
|
+
dot: :parse_dot_notation,
|
|
41
|
+
descendants: :parse_descendant_segment,
|
|
42
|
+
filter: :parse_filter_selector,
|
|
43
|
+
null: :parse_null,
|
|
44
|
+
}.freeze
|
|
19
45
|
LOWEST_PRECEDENCE = 0
|
|
20
46
|
OPERATOR_PRECEDENCE = {
|
|
21
47
|
',' => 0,
|
|
@@ -65,12 +91,8 @@ module Janeway
|
|
|
65
91
|
|
|
66
92
|
private
|
|
67
93
|
|
|
68
|
-
def pending_tokens?
|
|
69
|
-
@next_p < tokens.length
|
|
70
|
-
end
|
|
71
|
-
|
|
72
94
|
def next_not_terminator?
|
|
73
|
-
next_token && next_token.type != :
|
|
95
|
+
next_token && next_token.type != :eof
|
|
74
96
|
end
|
|
75
97
|
|
|
76
98
|
# Make "next" token become "current" by moving the pointer
|
|
@@ -135,33 +157,21 @@ module Janeway
|
|
|
135
157
|
end
|
|
136
158
|
|
|
137
159
|
def determine_parsing_function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
:parse_child_segment
|
|
149
|
-
elsif current.type == :dot # .
|
|
150
|
-
:parse_dot_notation
|
|
151
|
-
elsif current.type == :descendants # ..
|
|
152
|
-
:parse_descendant_segment
|
|
153
|
-
elsif current.type == :filter # ?
|
|
154
|
-
:parse_filter_selector
|
|
155
|
-
elsif current.type == :null # null
|
|
156
|
-
:parse_null
|
|
157
|
-
else
|
|
158
|
-
raise err("Don't know how to parse #{current}")
|
|
159
|
-
end
|
|
160
|
+
# Two type-driven tables plus a lexeme-driven check for unary operators.
|
|
161
|
+
# PARSE_METHOD_TYPES synthesizes the method name (`:parse_<type>`);
|
|
162
|
+
# PARSE_DISPATCH_BY_TYPE maps to a fixed method name.
|
|
163
|
+
return :"parse_#{current.type}" if PARSE_METHOD_TYPES.include?(current.type)
|
|
164
|
+
|
|
165
|
+
dispatch = PARSE_DISPATCH_BY_TYPE[current.type]
|
|
166
|
+
return dispatch if dispatch
|
|
167
|
+
return :parse_unary_operator if UNARY_OPERATOR_SET.include?(current.lexeme)
|
|
168
|
+
|
|
169
|
+
raise err("Don't know how to parse #{current}")
|
|
160
170
|
end
|
|
161
171
|
|
|
162
172
|
# @return [nil, Symbol]
|
|
163
173
|
def determine_infix_function(token = current)
|
|
164
|
-
return unless
|
|
174
|
+
return unless INFIX_OPERATOR_SET.include?(token.lexeme)
|
|
165
175
|
|
|
166
176
|
:parse_binary_operator
|
|
167
177
|
end
|
|
@@ -198,14 +208,16 @@ module Janeway
|
|
|
198
208
|
end
|
|
199
209
|
|
|
200
210
|
# '-' must be followed by a number token.
|
|
201
|
-
#
|
|
211
|
+
# Consume the minus, then replace the following number token in the
|
|
212
|
+
# token stream with a copy that has the negated literal. Replacing —
|
|
213
|
+
# rather than mutating in place — keeps Token immutable.
|
|
202
214
|
consume
|
|
203
|
-
parse_number
|
|
204
215
|
unless current.literal.is_a?(Numeric)
|
|
205
216
|
raise err("Minus operator \"-\" must be followed by number, got #{current.lexeme.inspect}")
|
|
206
217
|
end
|
|
207
218
|
|
|
208
|
-
|
|
219
|
+
original = current
|
|
220
|
+
@tokens[@next_p - 1] = Token.new(original.type, original.lexeme, -original.literal, original.location)
|
|
209
221
|
current
|
|
210
222
|
end
|
|
211
223
|
|
|
@@ -481,11 +493,10 @@ module Janeway
|
|
|
481
493
|
# Feed tokens to the FilterSelector until hitting a terminator
|
|
482
494
|
def parse_filter_selector
|
|
483
495
|
selector = AST::FilterSelector.new
|
|
484
|
-
|
|
485
|
-
while next_token && !terminator_types.include?(next_token.type)
|
|
496
|
+
while next_token && !TERMINATOR_TYPES.include?(next_token.type)
|
|
486
497
|
consume
|
|
487
498
|
node =
|
|
488
|
-
if
|
|
499
|
+
if BINARY_OPERATOR_SET.include?(current.lexeme)
|
|
489
500
|
parse_binary_operator(selector.value)
|
|
490
501
|
else
|
|
491
502
|
parse_expr_recursively
|
|
@@ -529,6 +540,7 @@ module Janeway
|
|
|
529
540
|
|
|
530
541
|
consume
|
|
531
542
|
op.right = parse_expr_recursively(op_precedence)
|
|
543
|
+
op.validate!
|
|
532
544
|
|
|
533
545
|
op
|
|
534
546
|
end
|