janeway-jsonpath 1.0.0 → 1.2.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/README.md +1 -1
- data/bin/janeway +4 -2
- 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/string_type.rb +4 -0
- 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 +30 -1
- data/lib/janeway/functions/count.rb +1 -8
- data/lib/janeway/functions/length.rb +1 -12
- data/lib/janeway/functions/match.rb +9 -18
- data/lib/janeway/functions/search.rb +11 -9
- data/lib/janeway/functions/value.rb +1 -7
- data/lib/janeway/functions.rb +130 -30
- 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/iregexp.rb +402 -0
- data/lib/janeway/lexer.rb +64 -79
- data/lib/janeway/location.rb +3 -1
- data/lib/janeway/normalized_path.rb +11 -2
- data/lib/janeway/parser.rb +47 -35
- data/lib/janeway/query.rb +47 -2
- data/lib/janeway/token.rb +40 -5
- data/lib/janeway/version.rb +1 -1
- metadata +2 -8
- 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
data/lib/janeway/error.rb
CHANGED
|
@@ -20,9 +20,38 @@ module Janeway
|
|
|
20
20
|
# @param query [String] entire query string
|
|
21
21
|
# @param location [Location] location of error
|
|
22
22
|
def initialize(msg, query = nil, location = nil)
|
|
23
|
-
|
|
23
|
+
prefix = 'Jsonpath query '
|
|
24
|
+
pointer = query.is_a?(String) && location
|
|
25
|
+
# The pointer is only right if the header's copy of the query is on one line.
|
|
26
|
+
# scrub swaps each invalid byte for one replacement character, so the columns stay valid
|
|
27
|
+
# and a query that is not valid UTF-8 cannot make building the error raise.
|
|
28
|
+
shown = query
|
|
29
|
+
if pointer
|
|
30
|
+
shown = shown.scrub unless shown.valid_encoding?
|
|
31
|
+
shown = shown.tr("\r\n", ' ')
|
|
32
|
+
end
|
|
33
|
+
full = +''
|
|
34
|
+
full << "#{prefix}#{shown}" if query
|
|
35
|
+
full << " (col #{location.col})" if location
|
|
36
|
+
full << ' - ' unless full.empty?
|
|
37
|
+
full << msg
|
|
38
|
+
full << pointer_line(shown, location, prefix.length) if pointer
|
|
39
|
+
super(full)
|
|
24
40
|
@query = query
|
|
25
41
|
@location = location
|
|
26
42
|
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Build the line of carets that marks the problem, aligned under the query in the message header.
|
|
47
|
+
# @param line [String] the query as shown in the header, on one line
|
|
48
|
+
# @param location [Location]
|
|
49
|
+
# @param offset [Integer] width of the text that comes before the query in the header
|
|
50
|
+
# @return [String] starts with a newline
|
|
51
|
+
def pointer_line(line, location, offset)
|
|
52
|
+
col = (location.col || 0).clamp(0, line.length)
|
|
53
|
+
length = (location.length || 1).clamp(1, [line.length - col, 1].max)
|
|
54
|
+
"\n#{' ' * (offset + col)}#{'^' * length}"
|
|
55
|
+
end
|
|
27
56
|
end
|
|
28
57
|
end
|
|
@@ -29,14 +29,7 @@ module Janeway
|
|
|
29
29
|
raise Error, "Invalid parameter - count() expects node list, got #{arg.value.inspect}" if arg.literal?
|
|
30
30
|
raise Error, 'Too many parameters for count() function call' unless current.type == :group_end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
AST::Function.new('count', parameters) do |node_list|
|
|
34
|
-
if node_list.is_a?(Array)
|
|
35
|
-
node_list.size
|
|
36
|
-
else
|
|
37
|
-
1 # the count of a non-empty singular nodelist such as count(@) is always 1.
|
|
38
|
-
end
|
|
39
|
-
end
|
|
32
|
+
AST::Function.new('count', parameters)
|
|
40
33
|
end
|
|
41
34
|
end
|
|
42
35
|
end
|
|
@@ -21,18 +21,7 @@ module Janeway
|
|
|
21
21
|
end
|
|
22
22
|
raise err('Too many parameters for length() function call') unless current.type == :group_end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
# * string - number of Unicode scalar values in the string.
|
|
26
|
-
# * array - number of elements in the array.
|
|
27
|
-
# * object - number of members in the object.
|
|
28
|
-
# For any other argument value, the result is the special result Nothing.
|
|
29
|
-
AST::Function.new('length', parameters) do |value|
|
|
30
|
-
if [Array, Hash, String].include?(value.class)
|
|
31
|
-
value.size
|
|
32
|
-
else
|
|
33
|
-
:nothing
|
|
34
|
-
end
|
|
35
|
-
end
|
|
24
|
+
AST::Function.new('length', parameters)
|
|
36
25
|
end
|
|
37
26
|
end
|
|
38
27
|
end
|
|
@@ -19,16 +19,10 @@ module Janeway
|
|
|
19
19
|
# otherwise.
|
|
20
20
|
#
|
|
21
21
|
#
|
|
22
|
-
# The
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# @see https://www.rfc-editor.org/rfc/rfc9485.html#name-pcre-re2-and-ruby-regexps
|
|
27
|
-
#
|
|
28
|
-
# The instructions are:
|
|
29
|
-
# * For any unescaped dots (.) outside character classes (first
|
|
30
|
-
# alternative of charClass production), replace the dot with [^\n\r].
|
|
31
|
-
# * Enclose the regexp in \A(?: and )\z.
|
|
22
|
+
# The pattern is validated and translated by Janeway::IRegexp (RFC 9485). A pattern that is not an
|
|
23
|
+
# I-Regexp gives LogicalFalse (RFC 9535 sections 2.4.6 and 2.4.7). A literal pattern that is over
|
|
24
|
+
# one of the limits in IRegexp (MAX_LENGTH, MAX_REPEAT, MAX_GROUP_DEPTH) raises Janeway::Error when
|
|
25
|
+
# the query is parsed. In a pattern ^ and $ are ordinary characters, as RFC 9485 has no anchors.
|
|
32
26
|
#
|
|
33
27
|
# tl;dr: How is this different from the search function?
|
|
34
28
|
# "match" must match the entire string, "search" matches a substring.
|
|
@@ -52,14 +46,11 @@ module Janeway
|
|
|
52
46
|
parameters << parse_function_parameter
|
|
53
47
|
raise Error, 'Too many parameters for match() function call' unless current.type == :group_end
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
false # result defined by RFC9535
|
|
61
|
-
end
|
|
62
|
-
end
|
|
49
|
+
validate_literal_pattern(parameters.last) # after the syntax checks, so they are reported first
|
|
50
|
+
|
|
51
|
+
# Body (including literal-regex fast path) is supplied by
|
|
52
|
+
# Functions::REGISTRY at FunctionInterpreter construction time.
|
|
53
|
+
AST::Function.new('match', parameters)
|
|
63
54
|
end
|
|
64
55
|
end
|
|
65
56
|
end
|
|
@@ -25,6 +25,11 @@ module Janeway
|
|
|
25
25
|
# the result is LogicalTrue if at least one such substring exists and is
|
|
26
26
|
# LogicalFalse otherwise.
|
|
27
27
|
#
|
|
28
|
+
# The pattern is validated and translated by Janeway::IRegexp (RFC 9485). A pattern that is not an
|
|
29
|
+
# I-Regexp gives LogicalFalse (RFC 9535 sections 2.4.6 and 2.4.7). A literal pattern that is over
|
|
30
|
+
# one of the limits in IRegexp (MAX_LENGTH, MAX_REPEAT, MAX_GROUP_DEPTH) raises Janeway::Error when
|
|
31
|
+
# the query is parsed. In a pattern ^ and $ are ordinary characters, as RFC 9485 has no anchors.
|
|
32
|
+
#
|
|
28
33
|
# tl;dr: How is this different from the match function?
|
|
29
34
|
# "match" must match the entire string, "search" matches a substring.
|
|
30
35
|
def parse_function_search
|
|
@@ -45,16 +50,13 @@ module Janeway
|
|
|
45
50
|
# Otherwise it is an expression that takes the regexp from the input document,
|
|
46
51
|
# and the iregexp will not be available until interpretation.
|
|
47
52
|
parameters << parse_function_parameter
|
|
48
|
-
raise Error, 'Too many parameters for
|
|
53
|
+
raise Error, 'Too many parameters for search() function call' unless current.type == :group_end
|
|
54
|
+
|
|
55
|
+
validate_literal_pattern(parameters.last) # after the syntax checks, so they are reported first
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
regexp.match?(str)
|
|
54
|
-
else
|
|
55
|
-
false # result defined by RFC9535
|
|
56
|
-
end
|
|
57
|
-
end
|
|
57
|
+
# Body (including literal-regex fast path) is supplied by
|
|
58
|
+
# Functions::REGISTRY at FunctionInterpreter construction time.
|
|
59
|
+
AST::Function.new('search', parameters)
|
|
58
60
|
end
|
|
59
61
|
end
|
|
60
62
|
end
|
|
@@ -36,13 +36,7 @@ module Janeway
|
|
|
36
36
|
parameters = [parse_function_parameter]
|
|
37
37
|
raise Error, 'Too many parameters for value() function call' unless current.type == :group_end
|
|
38
38
|
|
|
39
|
-
AST::Function.new('value', parameters)
|
|
40
|
-
if nodes.is_a?(Array) && nodes.size == 1
|
|
41
|
-
nodes.first
|
|
42
|
-
else
|
|
43
|
-
:nothing
|
|
44
|
-
end
|
|
45
|
-
end
|
|
39
|
+
AST::Function.new('value', parameters)
|
|
46
40
|
end
|
|
47
41
|
end
|
|
48
42
|
end
|
data/lib/janeway/functions.rb
CHANGED
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'ast/function'
|
|
4
|
+
require_relative 'ast/string_type'
|
|
5
|
+
require_relative 'iregexp'
|
|
4
6
|
|
|
5
7
|
module Janeway
|
|
6
|
-
# Mixin to provide JSONPath function handlers for Parser
|
|
8
|
+
# Mixin to provide JSONPath function handlers for Parser, plus the built-in function registry.
|
|
7
9
|
module Functions
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
chars.each_with_index do |char, i|
|
|
20
|
-
case char
|
|
21
|
-
when '[' then in_char_class = true
|
|
22
|
-
when ']'
|
|
23
|
-
in_char_class = false unless chars[i - 1] == '\\' # escaped ] does not close char class
|
|
24
|
-
when '.'
|
|
25
|
-
next if in_char_class || chars[i - 1] == '\\' # escaped dot
|
|
26
|
-
|
|
27
|
-
indexes << i # replace this dot
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
indexes.reverse_each do |i|
|
|
31
|
-
chars[i] = '[^\n\r]'
|
|
32
|
-
end
|
|
10
|
+
# Reject a literal pattern that exceeds an I-Regexp limit, at the point where the query is parsed.
|
|
11
|
+
#
|
|
12
|
+
# A literal that is not an I-Regexp is not an error: match() and search() return LogicalFalse
|
|
13
|
+
# for it, see build_regex_body. A literal over a limit is a valid I-Regexp that this library
|
|
14
|
+
# refuses to run, so the user is told, with the position in the query.
|
|
15
|
+
# @see https://www.rfc-editor.org/rfc/rfc9485.html#name-security-considerations
|
|
16
|
+
# RFC 9485 section 8: an implementation may limit range quantifiers in range and composability
|
|
17
|
+
# @param parameter [AST::Expression] the second parameter of match() or search(), just parsed
|
|
18
|
+
# @raise [Janeway::Error]
|
|
19
|
+
def validate_literal_pattern(parameter)
|
|
20
|
+
return unless parameter.is_a?(AST::StringType)
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
IRegexp.compile(parameter.value)
|
|
23
|
+
rescue IRegexp::Invalid
|
|
24
|
+
nil # RFC 9535 section 2.4.6: the function returns LogicalFalse, see build_regex_body
|
|
25
|
+
rescue IRegexp::LimitExceeded => e
|
|
26
|
+
raise Error.new("I-Regexp #{e.message}", @jsonpath, parameter.token&.location_of(e.offset, e.length))
|
|
37
27
|
end
|
|
38
28
|
|
|
39
29
|
# All jsonpath function parameters are one of these accepted types.
|
|
@@ -54,10 +44,120 @@ module Janeway
|
|
|
54
44
|
consume
|
|
55
45
|
result
|
|
56
46
|
end
|
|
47
|
+
|
|
48
|
+
# Largest number of distinct document-supplied patterns that one function call remembers.
|
|
49
|
+
DYNAMIC_PATTERN_CACHE_SIZE = 100
|
|
50
|
+
|
|
51
|
+
# Build a match/search body. When the pattern is a StringType literal, the parser has already
|
|
52
|
+
# validated it (Functions#validate_literal_pattern, which discards the result); the interpreter
|
|
53
|
+
# compiles it once more, with the anchoring this function needs, when it is built.
|
|
54
|
+
# Otherwise the pattern comes from the input document and is compiled when it is first used.
|
|
55
|
+
#
|
|
56
|
+
# A pattern that is not an I-Regexp, or that exceeds a limit, gives LogicalFalse.
|
|
57
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-match-function-extension
|
|
58
|
+
# RFC 9535 section 2.4.6: "If the first argument is not a string or the second argument is
|
|
59
|
+
# not a string conforming to [RFC9485], the result is LogicalFalse." Section 2.4.7 says the
|
|
60
|
+
# same for search().
|
|
61
|
+
# A literal that exceeds a limit has already been rejected by the parser, see
|
|
62
|
+
# Functions#validate_literal_pattern.
|
|
63
|
+
def self.build_regex_body(parameters, anchor:)
|
|
64
|
+
literal = parameters[1].is_a?(AST::StringType) ? parameters[1].value : nil
|
|
65
|
+
# The cache holds compiled regexps keyed by pattern text, so reusing it for other subjects is
|
|
66
|
+
# correct. It lives as long as the interpreter tree, which Query#interpreter_tree_for caches
|
|
67
|
+
# on an unfrozen Query.
|
|
68
|
+
cache = {} # pattern string => Regexp, or nil when the pattern is unusable
|
|
69
|
+
cache[literal] = compile_or_nil(literal, anchor) if literal # a literal is compiled once, now
|
|
70
|
+
lambda do |str, pattern|
|
|
71
|
+
pattern = literal if literal
|
|
72
|
+
next false unless str.is_a?(String) && pattern.is_a?(String)
|
|
73
|
+
# A pattern in the wrong encoding is false and never reaches the cache: Hash lookup compares
|
|
74
|
+
# strings by content, so a binary 'a' would otherwise share the entry of a UTF-8 'a'.
|
|
75
|
+
next false unless IRegexp.valid_encoding?(pattern)
|
|
76
|
+
# RFC 9485 section 8 lets an implementation limit patterns; the limit is this library's own
|
|
77
|
+
# choice. Checked before parsing so that a huge pattern costs almost nothing.
|
|
78
|
+
next false if pattern.length > IRegexp::MAX_LENGTH
|
|
79
|
+
|
|
80
|
+
unless cache.key?(pattern)
|
|
81
|
+
cache.shift if cache.size >= DYNAMIC_PATTERN_CACHE_SIZE
|
|
82
|
+
cache[pattern] = compile_or_nil(pattern, anchor)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
!cache[pattern].nil? && match_safely(cache[pattern], str)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# RFC 9535 section 2.4.6: a pattern that does not conform to RFC 9485 gives LogicalFalse, so it is nil here.
|
|
90
|
+
# A pattern over a limit (RFC 9485 section 8) is treated the same way.
|
|
91
|
+
# @return [Regexp, nil] nil when the pattern is not usable, see build_regex_body
|
|
92
|
+
def self.compile_or_nil(pattern, anchor)
|
|
93
|
+
IRegexp.compile(pattern, anchor: anchor)
|
|
94
|
+
rescue IRegexp::Invalid, IRegexp::LimitExceeded
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Match, and treat a match that Ruby cannot or will not complete as no match.
|
|
99
|
+
#
|
|
100
|
+
# Deliberate deviation from RFC 9535 sections 2.4.6 and 2.4.7, which say the result is LogicalTrue or
|
|
101
|
+
# LogicalFalse and say nothing about a match that cannot complete: a timeout, or a subject that is
|
|
102
|
+
# not valid UTF-8, gives false. RFC 9485 section 8 (security considerations) is why a timeout exists.
|
|
103
|
+
# @param regexp [Regexp]
|
|
104
|
+
# @param str [String]
|
|
105
|
+
# @return [Boolean]
|
|
106
|
+
def self.match_safely(regexp, str)
|
|
107
|
+
regexp.match?(str)
|
|
108
|
+
rescue IRegexp::TIMEOUT_ERROR, ArgumentError, Encoding::CompatibilityError
|
|
109
|
+
# TIMEOUT_ERROR: the match took longer than IRegexp::MATCH_TIMEOUT.
|
|
110
|
+
# ArgumentError, Encoding::CompatibilityError: str is not valid UTF-8.
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Metadata + body builder for every built-in JSONPath function.
|
|
115
|
+
#
|
|
116
|
+
# arity — number of parameters
|
|
117
|
+
# literal_return — true when the result is a literal (see RFC 9535
|
|
118
|
+
# well-typedness of function extensions)
|
|
119
|
+
# build — proc that takes the parsed `parameters` list and
|
|
120
|
+
# returns the body proc (parameters allow per-instance
|
|
121
|
+
# optimization, e.g. literal-regex compilation)
|
|
122
|
+
REGISTRY = {
|
|
123
|
+
'count' => {
|
|
124
|
+
arity: 1,
|
|
125
|
+
literal_return: true,
|
|
126
|
+
build: ->(_) { ->(nodes) { nodes.is_a?(Array) ? nodes.size : 1 } },
|
|
127
|
+
},
|
|
128
|
+
'length' => {
|
|
129
|
+
arity: 1,
|
|
130
|
+
literal_return: true,
|
|
131
|
+
build: lambda { |_|
|
|
132
|
+
lambda { |value|
|
|
133
|
+
[Array, Hash, String].include?(value.class) ? value.size : :nothing
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
'value' => {
|
|
138
|
+
arity: 1,
|
|
139
|
+
literal_return: true,
|
|
140
|
+
build: lambda { |_|
|
|
141
|
+
lambda { |nodes|
|
|
142
|
+
nodes.is_a?(Array) && nodes.size == 1 ? nodes.first : :nothing
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
'match' => {
|
|
147
|
+
arity: 2,
|
|
148
|
+
literal_return: false,
|
|
149
|
+
build: ->(parameters) { Functions.build_regex_body(parameters, anchor: true) },
|
|
150
|
+
},
|
|
151
|
+
'search' => {
|
|
152
|
+
arity: 2,
|
|
153
|
+
literal_return: false,
|
|
154
|
+
build: ->(parameters) { Functions.build_regex_body(parameters, anchor: false) },
|
|
155
|
+
},
|
|
156
|
+
}.freeze
|
|
57
157
|
end
|
|
58
158
|
end
|
|
59
159
|
|
|
60
|
-
# Require function definitions
|
|
160
|
+
# Require function parse-method definitions
|
|
61
161
|
Dir.children("#{__dir__}/functions/").each do |path|
|
|
62
162
|
require_relative "functions/#{path[0..-4]}"
|
|
63
163
|
end
|
data/lib/janeway/interpreter.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Janeway
|
|
|
33
33
|
@query = query
|
|
34
34
|
@type = as
|
|
35
35
|
@jsonpath = query.jsonpath
|
|
36
|
-
@root = query_to_interpreter_tree(@query, &block)
|
|
36
|
+
@root = query.interpreter_tree_for(@type) { query_to_interpreter_tree(@query, &block) }
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# Return multiline JSON string describing the interpreter tree.
|
|
@@ -51,65 +51,84 @@ module Janeway
|
|
|
51
51
|
# @return [Object]
|
|
52
52
|
def interpret(input)
|
|
53
53
|
@root.interpret(nil, nil, input, [])
|
|
54
|
-
rescue
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
rescue Janeway::Error => e
|
|
55
|
+
# Reformat known interpretation errors to include the query in the message.
|
|
56
|
+
# Real bugs (NoMethodError, TypeError, NameError, ...) are intentionally
|
|
57
|
+
# NOT caught here — they should surface with a real backtrace, not be
|
|
58
|
+
# masked as "Jsonpath query ..." messages.
|
|
59
|
+
raise if e.query # already annotated
|
|
60
|
+
|
|
61
|
+
annotated = err(e.message)
|
|
62
|
+
annotated.set_backtrace e.backtrace
|
|
63
|
+
raise annotated
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
private
|
|
62
67
|
|
|
63
68
|
# Build a tree of interpreter classes based on the query's abstract syntax tree.
|
|
64
69
|
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
70
|
+
# Four phases, one per private helper:
|
|
71
|
+
# 1. build_interpreters — one interpreter per AST node
|
|
72
|
+
# 2. append_terminal — Yielder / Deleter / DeleteIf for the caller's mode
|
|
73
|
+
# 3. link_next_pointers — wire each interpreter to the next one
|
|
74
|
+
# 4. rewire_child_segments — push tail selectors into each ChildSegmentInterpreter
|
|
75
|
+
# (branch rewrite explained in the docstring at the end of this file)
|
|
67
76
|
#
|
|
68
77
|
# @return [Interpreters::RootNodeInterpreter]
|
|
69
78
|
def query_to_interpreter_tree(query, &block)
|
|
70
|
-
|
|
71
|
-
interpreters
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
interpreters = build_interpreters(query)
|
|
80
|
+
append_terminal(interpreters, &block)
|
|
81
|
+
link_next_pointers(interpreters)
|
|
82
|
+
rewire_child_segments(interpreters)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Phase 1: one interpreter per AST node in the query's top-level chain.
|
|
86
|
+
def build_interpreters(query)
|
|
87
|
+
query.node_list.map { |node| Interpreters::TreeConstructor.ast_node_to_interpreter(node) }
|
|
88
|
+
end
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
# Phase 2: append the terminal interpreter that matches the caller's mode.
|
|
91
|
+
# Deleter / delete_if REPLACE the last selector (the terminal semantics
|
|
92
|
+
# live in the leaf itself); iterator APPENDS a Yielder after it.
|
|
93
|
+
def append_terminal(interpreters, &block)
|
|
77
94
|
case @type
|
|
78
95
|
when :iterator then interpreters.push(Yielder.new(&block))
|
|
79
|
-
when :deleter then interpreters.push
|
|
80
|
-
when :delete_if then interpreters.push
|
|
96
|
+
when :deleter then interpreters.push(make_deleter(interpreters.pop))
|
|
97
|
+
when :delete_if then interpreters.push(make_delete_if(interpreters.pop, &block))
|
|
81
98
|
end
|
|
99
|
+
end
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
# Phase 3: link `.next` from each interpreter to the following one.
|
|
102
|
+
def link_next_pointers(interpreters)
|
|
84
103
|
interpreters.each_with_index do |node, i|
|
|
85
104
|
node.next = interpreters[i + 1]
|
|
86
105
|
end
|
|
106
|
+
end
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# For full explanation see the explanation at the end of this file.
|
|
95
|
-
selectors = []
|
|
108
|
+
# Phase 4: for every ChildSegmentInterpreter, remove the interpreters that
|
|
109
|
+
# follow it in the flat chain and push them into the child segment as
|
|
110
|
+
# branches. Work backwards so nested child segments compose correctly.
|
|
111
|
+
# @return [Interpreters::Base] root of the resulting tree
|
|
112
|
+
def rewire_child_segments(interpreters)
|
|
113
|
+
pending = []
|
|
96
114
|
interpreters.reverse_each do |node|
|
|
97
115
|
if node.is_a?(Interpreters::ChildSegmentInterpreter)
|
|
98
116
|
node.next = nil
|
|
99
|
-
node.push(
|
|
100
|
-
|
|
117
|
+
node.push(pending.pop) until pending.empty?
|
|
118
|
+
pending = [node]
|
|
101
119
|
else
|
|
102
|
-
|
|
120
|
+
pending << node
|
|
103
121
|
end
|
|
104
122
|
end
|
|
105
|
-
|
|
106
|
-
selectors.last
|
|
123
|
+
pending.last
|
|
107
124
|
end
|
|
108
125
|
|
|
109
126
|
# Make a Deleter that will delete the results matched by a Selector.
|
|
127
|
+
# Implemented as a DeleteIf with the PASS_ALL sentinel block — the
|
|
128
|
+
# separate Deleter classes were folded into DeleteIf.
|
|
110
129
|
# @param interpreter [Interpreters::Base] interpeter subclass
|
|
111
130
|
def make_deleter(interpreter)
|
|
112
|
-
TreeConstructor.
|
|
131
|
+
TreeConstructor.ast_node_to_delete_if(interpreter.node, &Interpreters::IterationHelper::PASS_ALL)
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
# Make a DeleteIf that will delete the results matched by a Selector,
|
|
@@ -27,19 +27,13 @@ module Janeway
|
|
|
27
27
|
# @return [Array]
|
|
28
28
|
def interpret(input, _parent, _root, path)
|
|
29
29
|
return [] unless input.is_a?(Array)
|
|
30
|
-
return [] if
|
|
30
|
+
return [] if effective_step.zero? # RFC: When step is 0, no elements are selected.
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Convert bounds and step to index values.
|
|
37
|
-
# Omit the final index, since no value is collected for that.
|
|
38
|
-
# Delete indexes from largest to smallest, so that deleting an index does
|
|
39
|
-
# not change the remaining indexes
|
|
32
|
+
# Delete matching indexes from largest to smallest so that deletion
|
|
33
|
+
# does not shift the remaining indexes.
|
|
34
|
+
indexes = index_range(input.size)
|
|
40
35
|
results = []
|
|
41
|
-
if
|
|
42
|
-
indexes = lower.step(to: upper - 1, by: selector.step).to_a
|
|
36
|
+
if effective_step.positive?
|
|
43
37
|
indexes.reverse_each do |i|
|
|
44
38
|
next unless @yield_proc.call(input[i], input, path + [i])
|
|
45
39
|
|
|
@@ -47,8 +41,11 @@ module Janeway
|
|
|
47
41
|
end
|
|
48
42
|
results.reverse
|
|
49
43
|
else
|
|
50
|
-
indexes
|
|
51
|
-
|
|
44
|
+
indexes.each do |i|
|
|
45
|
+
next unless @yield_proc.call(input[i], input, path + [i])
|
|
46
|
+
|
|
47
|
+
results << input.delete_at(i)
|
|
48
|
+
end
|
|
52
49
|
results
|
|
53
50
|
end
|
|
54
51
|
end
|
|
@@ -17,19 +17,9 @@ module Janeway
|
|
|
17
17
|
# @return [Array]
|
|
18
18
|
def interpret(input, _parent, root, path)
|
|
19
19
|
return [] unless input.is_a?(Array)
|
|
20
|
-
return [] if
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
lower = selector.lower_index(input.size)
|
|
24
|
-
upper = selector.upper_index(input.size)
|
|
25
|
-
|
|
26
|
-
# Collect real index values. Omit the final index, since no value is collected for that.
|
|
27
|
-
indexes =
|
|
28
|
-
if selector.step.positive?
|
|
29
|
-
lower.step(to: upper - 1, by: selector.step).to_a
|
|
30
|
-
else
|
|
31
|
-
upper.step(to: lower + 1, by: selector.step).to_a
|
|
32
|
-
end
|
|
20
|
+
return [] if effective_step.zero? # RFC: When step is 0, no elements are selected.
|
|
21
|
+
|
|
22
|
+
indexes = index_range(input.size)
|
|
33
23
|
return indexes.map { |i| input[i] } unless @next
|
|
34
24
|
|
|
35
25
|
# Apply child selector to each node in the output node list
|
|
@@ -44,6 +34,59 @@ module Janeway
|
|
|
44
34
|
def as_json
|
|
45
35
|
{ type: type, value: node.to_s, next: @next&.as_json }.compact
|
|
46
36
|
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
# Concrete list of source-array indexes this slice selects, in
|
|
41
|
+
# iteration order. Consumed by both this interpreter and
|
|
42
|
+
# ArraySliceSelectorDeleteIf.
|
|
43
|
+
#
|
|
44
|
+
# @param input_size [Integer]
|
|
45
|
+
# @return [Array<Integer>]
|
|
46
|
+
def index_range(input_size)
|
|
47
|
+
lower, upper = bounds(input_size)
|
|
48
|
+
if effective_step.positive?
|
|
49
|
+
lower.step(to: upper - 1, by: effective_step).to_a
|
|
50
|
+
else
|
|
51
|
+
upper.step(to: lower + 1, by: effective_step).to_a
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Effective step: raw @step from the AST, or 1 if unspecified.
|
|
56
|
+
def effective_step
|
|
57
|
+
selector.step || 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compute lower/upper bounds for the slice given the input array size.
|
|
61
|
+
# RFC 9535 pseudocode lives here (formerly on AST::ArraySliceSelector).
|
|
62
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.3.4.2.2
|
|
63
|
+
#
|
|
64
|
+
# @param input_size [Integer]
|
|
65
|
+
# @return [Array(Integer, Integer)] [lower, upper]
|
|
66
|
+
def bounds(input_size)
|
|
67
|
+
if effective_step >= 0
|
|
68
|
+
start = selector.start || 0
|
|
69
|
+
end_ = selector.end || input_size
|
|
70
|
+
else
|
|
71
|
+
start = selector.start || (input_size - 1)
|
|
72
|
+
end_ = selector.end || ((-1 * input_size) - 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
n_start = normalize(start, input_size)
|
|
76
|
+
n_end = normalize(end_, input_size)
|
|
77
|
+
|
|
78
|
+
if effective_step >= 0
|
|
79
|
+
[n_start.clamp(0, input_size), n_end.clamp(0, input_size)]
|
|
80
|
+
else
|
|
81
|
+
[n_end.clamp(-1, input_size - 1), n_start.clamp(-1, input_size - 1)]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# IETF: slice parameters must be normalized before use as bounds —
|
|
86
|
+
# negative values count from the end of the array.
|
|
87
|
+
def normalize(index, len)
|
|
88
|
+
index >= 0 ? index : len + index
|
|
89
|
+
end
|
|
47
90
|
end
|
|
48
91
|
end
|
|
49
92
|
end
|
|
@@ -13,6 +13,18 @@ module Janeway
|
|
|
13
13
|
super
|
|
14
14
|
@left = TreeConstructor.ast_node_to_interpreter(operator.left)
|
|
15
15
|
@right = TreeConstructor.ast_node_to_interpreter(operator.right)
|
|
16
|
+
|
|
17
|
+
# Bind the per-op behavior at construction time to avoid dispatching
|
|
18
|
+
# on operator.name and allocating a symbol on every candidate value.
|
|
19
|
+
case operator.name
|
|
20
|
+
when :and, :or
|
|
21
|
+
@extract_single_value = false
|
|
22
|
+
when :equal, :not_equal, :less_than, :greater_than, :less_than_or_equal, :greater_than_or_equal
|
|
23
|
+
@extract_single_value = true
|
|
24
|
+
else
|
|
25
|
+
raise "Don't know how to handle binary operator #{operator.name.inspect}"
|
|
26
|
+
end
|
|
27
|
+
@op = method(:"interpret_#{operator.name}")
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
# The binary operators are all comparison operators that test equality.
|
|
@@ -28,20 +40,13 @@ module Janeway
|
|
|
28
40
|
# @param root [Array, Hash] the entire input
|
|
29
41
|
# @param _path [Array] ignored
|
|
30
42
|
def interpret(input, parent, root, _path = nil)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
rhs = @right.interpret(input, parent, root, [])
|
|
37
|
-
when :equal, :not_equal, :less_than, :greater_than, :less_than_or_equal, :greater_than_or_equal
|
|
38
|
-
# handle node values for comparison check
|
|
39
|
-
lhs = to_single_value @left.interpret(input, parent, root, [])
|
|
40
|
-
rhs = to_single_value @right.interpret(input, parent, root, [])
|
|
41
|
-
else
|
|
42
|
-
raise "Don't know how to handle binary operator #{operator.name.inspect}"
|
|
43
|
+
lhs = @left.interpret(input, parent, root, [])
|
|
44
|
+
rhs = @right.interpret(input, parent, root, [])
|
|
45
|
+
if @extract_single_value
|
|
46
|
+
lhs = to_single_value(lhs)
|
|
47
|
+
rhs = to_single_value(rhs)
|
|
43
48
|
end
|
|
44
|
-
|
|
49
|
+
@op.call(lhs, rhs)
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
# Interpret a node and extract its value, in preparation for using the node
|