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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- 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 +3 -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 +35 -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 +14 -7
- 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 -35
- 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 +1 -0
- data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -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 +66 -77
- data/lib/janeway/location.rb +3 -1
- data/lib/janeway/normalized_path.rb +23 -7
- data/lib/janeway/parser.rb +46 -34
- data/lib/janeway/query.rb +48 -5
- data/lib/janeway/token.rb +2 -5
- data/lib/janeway/version.rb +1 -1
- data/lib/janeway.rb +16 -3
- metadata +3 -10
- 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,7 +20,12 @@ 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
|
+
full = +''
|
|
24
|
+
full << "Jsonpath query #{query}" if query
|
|
25
|
+
full << " (col #{location.col})" if location
|
|
26
|
+
full << ' - ' unless full.empty?
|
|
27
|
+
full << msg
|
|
28
|
+
super(full)
|
|
24
29
|
@query = query
|
|
25
30
|
@location = location
|
|
26
31
|
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
|
|
@@ -52,14 +52,9 @@ module Janeway
|
|
|
52
52
|
parameters << parse_function_parameter
|
|
53
53
|
raise Error, 'Too many parameters for match() function call' unless current.type == :group_end
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
regexp.match?(str)
|
|
59
|
-
else
|
|
60
|
-
false # result defined by RFC9535
|
|
61
|
-
end
|
|
62
|
-
end
|
|
55
|
+
# Body (including literal-regex fast path) is supplied by
|
|
56
|
+
# Functions::REGISTRY at FunctionInterpreter construction time.
|
|
57
|
+
AST::Function.new('match', parameters)
|
|
63
58
|
end
|
|
64
59
|
end
|
|
65
60
|
end
|
|
@@ -47,14 +47,9 @@ module Janeway
|
|
|
47
47
|
parameters << parse_function_parameter
|
|
48
48
|
raise Error, 'Too many parameters for match() function call' unless current.type == :group_end
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
regexp.match?(str)
|
|
54
|
-
else
|
|
55
|
-
false # result defined by RFC9535
|
|
56
|
-
end
|
|
57
|
-
end
|
|
50
|
+
# Body (including literal-regex fast path) is supplied by
|
|
51
|
+
# Functions::REGISTRY at FunctionInterpreter construction time.
|
|
52
|
+
AST::Function.new('search', parameters)
|
|
58
53
|
end
|
|
59
54
|
end
|
|
60
55
|
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,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'ast/function'
|
|
4
|
+
require_relative 'ast/string_type'
|
|
4
5
|
|
|
5
6
|
module Janeway
|
|
6
|
-
# Mixin to provide JSONPath function handlers for Parser
|
|
7
|
+
# Mixin to provide JSONPath function handlers for Parser, plus the shared
|
|
8
|
+
# I-Regexp translation helper and the built-in function registry.
|
|
7
9
|
module Functions
|
|
8
10
|
# Convert IRegexp format to ruby regexp equivalent, following the instructions in rfc9485.
|
|
9
11
|
# @see https://www.rfc-editor.org/rfc/rfc9485.html#name-pcre-re2-and-ruby-regexps
|
|
@@ -35,6 +37,7 @@ module Janeway
|
|
|
35
37
|
regex_str = anchor ? format('\A(?:%s)\z', chars.join) : chars.join
|
|
36
38
|
Regexp.new(regex_str)
|
|
37
39
|
end
|
|
40
|
+
module_function :translate_iregex_to_ruby_regex
|
|
38
41
|
|
|
39
42
|
# All jsonpath function parameters are one of these accepted types.
|
|
40
43
|
# Parse the function parameter and return the result.
|
|
@@ -54,10 +57,80 @@ module Janeway
|
|
|
54
57
|
consume
|
|
55
58
|
result
|
|
56
59
|
end
|
|
60
|
+
|
|
61
|
+
# Build a match/search body. When the pattern is a StringType literal we
|
|
62
|
+
# compile the regex once at parse time (see perf pass, commit fd8b92a).
|
|
63
|
+
# Otherwise fall back to per-call compilation.
|
|
64
|
+
def self.build_regex_body(parameters, anchor:)
|
|
65
|
+
literal_regexp =
|
|
66
|
+
if parameters[1].is_a?(AST::StringType)
|
|
67
|
+
begin
|
|
68
|
+
translate_iregex_to_ruby_regex(parameters[1].value, anchor: anchor)
|
|
69
|
+
rescue RegexpError
|
|
70
|
+
nil # fall back to per-call compilation, which will raise at eval time
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if literal_regexp
|
|
75
|
+
->(str, _pattern) { str.is_a?(String) && literal_regexp.match?(str) }
|
|
76
|
+
else
|
|
77
|
+
lambda do |str, pattern|
|
|
78
|
+
if str.is_a?(String) && pattern.is_a?(String)
|
|
79
|
+
translate_iregex_to_ruby_regex(pattern, anchor: anchor).match?(str)
|
|
80
|
+
else
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Metadata + body builder for every built-in JSONPath function.
|
|
88
|
+
#
|
|
89
|
+
# arity — number of parameters
|
|
90
|
+
# literal_return — true when the result is a literal (see RFC 9535
|
|
91
|
+
# well-typedness of function extensions)
|
|
92
|
+
# build — proc that takes the parsed `parameters` list and
|
|
93
|
+
# returns the body proc (parameters allow per-instance
|
|
94
|
+
# optimization, e.g. literal-regex compilation)
|
|
95
|
+
REGISTRY = {
|
|
96
|
+
'count' => {
|
|
97
|
+
arity: 1,
|
|
98
|
+
literal_return: true,
|
|
99
|
+
build: ->(_) { ->(nodes) { nodes.is_a?(Array) ? nodes.size : 1 } },
|
|
100
|
+
},
|
|
101
|
+
'length' => {
|
|
102
|
+
arity: 1,
|
|
103
|
+
literal_return: true,
|
|
104
|
+
build: lambda { |_|
|
|
105
|
+
lambda { |value|
|
|
106
|
+
[Array, Hash, String].include?(value.class) ? value.size : :nothing
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
'value' => {
|
|
111
|
+
arity: 1,
|
|
112
|
+
literal_return: true,
|
|
113
|
+
build: lambda { |_|
|
|
114
|
+
lambda { |nodes|
|
|
115
|
+
nodes.is_a?(Array) && nodes.size == 1 ? nodes.first : :nothing
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
'match' => {
|
|
120
|
+
arity: 2,
|
|
121
|
+
literal_return: false,
|
|
122
|
+
build: ->(parameters) { Functions.build_regex_body(parameters, anchor: true) },
|
|
123
|
+
},
|
|
124
|
+
'search' => {
|
|
125
|
+
arity: 2,
|
|
126
|
+
literal_return: false,
|
|
127
|
+
build: ->(parameters) { Functions.build_regex_body(parameters, anchor: false) },
|
|
128
|
+
},
|
|
129
|
+
}.freeze
|
|
57
130
|
end
|
|
58
131
|
end
|
|
59
132
|
|
|
60
|
-
# Require function definitions
|
|
133
|
+
# Require function parse-method definitions
|
|
61
134
|
Dir.children("#{__dir__}/functions/").each do |path|
|
|
62
135
|
require_relative "functions/#{path[0..-4]}"
|
|
63
136
|
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.
|
|
@@ -50,70 +50,85 @@ module Janeway
|
|
|
50
50
|
# @param input [Array, Hash] object to be searched
|
|
51
51
|
# @return [Object]
|
|
52
52
|
def interpret(input)
|
|
53
|
-
unless input.is_a?(Hash) || input.is_a?(Array)
|
|
54
|
-
return [] # can't query on any other types, but need to check because a string is also valid json
|
|
55
|
-
end
|
|
56
|
-
|
|
57
53
|
@root.interpret(nil, nil, input, [])
|
|
58
|
-
rescue
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
private
|
|
66
67
|
|
|
67
68
|
# Build a tree of interpreter classes based on the query's abstract syntax tree.
|
|
68
69
|
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
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)
|
|
71
76
|
#
|
|
72
77
|
# @return [Interpreters::RootNodeInterpreter]
|
|
73
78
|
def query_to_interpreter_tree(query, &block)
|
|
74
|
-
|
|
75
|
-
interpreters
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
interpreters = build_interpreters(query)
|
|
80
|
+
append_terminal(interpreters, &block)
|
|
81
|
+
link_next_pointers(interpreters)
|
|
82
|
+
rewire_child_segments(interpreters)
|
|
83
|
+
end
|
|
79
84
|
|
|
80
|
-
|
|
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
|
|
89
|
+
|
|
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)
|
|
81
94
|
case @type
|
|
82
95
|
when :iterator then interpreters.push(Yielder.new(&block))
|
|
83
|
-
when :deleter then interpreters.push
|
|
84
|
-
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))
|
|
85
98
|
end
|
|
99
|
+
end
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
# Phase 3: link `.next` from each interpreter to the following one.
|
|
102
|
+
def link_next_pointers(interpreters)
|
|
88
103
|
interpreters.each_with_index do |node, i|
|
|
89
104
|
node.next = interpreters[i + 1]
|
|
90
105
|
end
|
|
106
|
+
end
|
|
91
107
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# For full explanation see the explanation at the end of this file.
|
|
99
|
-
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 = []
|
|
100
114
|
interpreters.reverse_each do |node|
|
|
101
115
|
if node.is_a?(Interpreters::ChildSegmentInterpreter)
|
|
102
116
|
node.next = nil
|
|
103
|
-
node.push(
|
|
104
|
-
|
|
117
|
+
node.push(pending.pop) until pending.empty?
|
|
118
|
+
pending = [node]
|
|
105
119
|
else
|
|
106
|
-
|
|
120
|
+
pending << node
|
|
107
121
|
end
|
|
108
122
|
end
|
|
109
|
-
|
|
110
|
-
selectors.last
|
|
123
|
+
pending.last
|
|
111
124
|
end
|
|
112
125
|
|
|
113
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.
|
|
114
129
|
# @param interpreter [Interpreters::Base] interpeter subclass
|
|
115
130
|
def make_deleter(interpreter)
|
|
116
|
-
TreeConstructor.
|
|
131
|
+
TreeConstructor.ast_node_to_delete_if(interpreter.node, &Interpreters::IterationHelper::PASS_ALL)
|
|
117
132
|
end
|
|
118
133
|
|
|
119
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
|
|
@@ -21,23 +21,42 @@ module Janeway
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Visit all descendants of `input
|
|
25
|
-
#
|
|
24
|
+
# Visit all descendants of `input` and concatenate the results of
|
|
25
|
+
# `block` at each node. Iterative depth-first pre-order — a recursive
|
|
26
|
+
# implementation would risk SystemStackError on deep JSON and allocated
|
|
27
|
+
# an array-per-node plus a flatten pass.
|
|
28
|
+
#
|
|
26
29
|
# @param input [Array, Hash] the results of processing so far
|
|
27
|
-
# @param path [Array<String>] elements of normalized path to the current input
|
|
28
30
|
# @param parent [Array, Hash] parent of the input object
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
|
32
|
+
def visit(input, parent, path)
|
|
33
|
+
results = []
|
|
34
|
+
stack = [[input, parent, path]]
|
|
35
|
+
until stack.empty?
|
|
36
|
+
node, node_parent, node_path = stack.pop
|
|
37
|
+
results.concat(yield(node, node_parent, node_path))
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
case node
|
|
40
|
+
when Array
|
|
41
|
+
# Push in reverse so the leftmost child is popped first (pre-order).
|
|
42
|
+
i = node.size - 1
|
|
43
|
+
while i >= 0
|
|
44
|
+
stack.push([node[i], node, node_path + [i]])
|
|
45
|
+
i -= 1
|
|
46
|
+
end
|
|
47
|
+
when Hash
|
|
48
|
+
# Iterate to an array once so we can walk it in reverse.
|
|
49
|
+
pairs = node.to_a
|
|
50
|
+
i = pairs.size - 1
|
|
51
|
+
while i >= 0
|
|
52
|
+
k, v = pairs[i]
|
|
53
|
+
stack.push([v, node, node_path + [k]])
|
|
54
|
+
i -= 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
# Basic (non-container) types have no descendants — nothing to push.
|
|
37
58
|
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
results.flatten(1).compact
|
|
59
|
+
results
|
|
41
60
|
end
|
|
42
61
|
end
|
|
43
62
|
end
|
|
@@ -43,27 +43,9 @@ module Janeway
|
|
|
43
43
|
# @param root [Array, Hash] the entire input
|
|
44
44
|
# @param path [Array<String>] elements of normalized path to the current input
|
|
45
45
|
def interpret_hash(input, root, path)
|
|
46
|
-
# Apply filter expressions to the input data
|
|
47
46
|
node_list = []
|
|
48
|
-
input.each
|
|
49
|
-
|
|
50
|
-
result = @expr.interpret(value, nil, root, [])
|
|
51
|
-
case result
|
|
52
|
-
when TrueClass then node_list << [key, value] # comparison test - pass
|
|
53
|
-
when FalseClass then nil # comparison test - fail
|
|
54
|
-
when Array then node_list << [key, value] unless result.empty? # existence test - node list
|
|
55
|
-
else
|
|
56
|
-
node_list << [key, value] # existence test. Null values here == success.
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
return node_list.map(&:last) unless @next
|
|
60
|
-
|
|
61
|
-
# Apply child selector to each node in the output node list
|
|
62
|
-
results = []
|
|
63
|
-
node_list.each do |key, value|
|
|
64
|
-
results.concat @next.interpret(value, input, root, path + [key])
|
|
65
|
-
end
|
|
66
|
-
results
|
|
47
|
+
input.each { |key, value| node_list << [key, value] if filter_match?(value, root) }
|
|
48
|
+
forward_matches(node_list, input, root, path)
|
|
67
49
|
end
|
|
68
50
|
|
|
69
51
|
# Interpret selector on the input.
|
|
@@ -71,29 +53,39 @@ module Janeway
|
|
|
71
53
|
# @param root [Array, Hash] the entire input
|
|
72
54
|
# @param path [Array<String>] elements of normalized path to the current input
|
|
73
55
|
def interpret_array(input, root, path)
|
|
74
|
-
# Apply filter expressions to the input data
|
|
75
56
|
node_list = []
|
|
76
|
-
input.each_with_index
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
57
|
+
input.each_with_index { |value, i| node_list << [i, value] if filter_match?(value, root) }
|
|
58
|
+
forward_matches(node_list, input, root, path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Classify the result of running the filter expression against a value.
|
|
64
|
+
# Returns true if the value should be kept.
|
|
65
|
+
def filter_match?(value, root)
|
|
66
|
+
result = @expr.interpret(value, nil, root, [])
|
|
67
|
+
case result
|
|
68
|
+
when TrueClass then true # comparison test - pass
|
|
69
|
+
when FalseClass then false # comparison test - fail
|
|
70
|
+
when Array then !result.empty? # existence test - non-empty node list
|
|
71
|
+
else true # existence test - null / other values count as success
|
|
86
72
|
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Given a node_list of [key_or_index, value] pairs, apply @next to each
|
|
76
|
+
# and concat the results. If no @next, return just the values.
|
|
77
|
+
def forward_matches(node_list, input, root, path)
|
|
87
78
|
return node_list.map(&:last) unless @next
|
|
88
79
|
|
|
89
|
-
# Apply child selector to each node in the output node list
|
|
90
80
|
results = []
|
|
91
|
-
node_list.each do |
|
|
92
|
-
results.concat @next.interpret(value, input, root, path + [
|
|
81
|
+
node_list.each do |key, value|
|
|
82
|
+
results.concat @next.interpret(value, input, root, path + [key])
|
|
93
83
|
end
|
|
94
84
|
results
|
|
95
85
|
end
|
|
96
86
|
|
|
87
|
+
public
|
|
88
|
+
|
|
97
89
|
# @return [Hash]
|
|
98
90
|
def as_json
|
|
99
91
|
{ type: type, value: node.to_s, next: @next&.as_json }.compact
|