janeway-jsonpath 0.3.0 → 0.4.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 +201 -28
- data/bin/janeway +36 -9
- data/lib/janeway/ast/expression.rb +1 -2
- data/lib/janeway/enumerator.rb +43 -0
- data/lib/janeway/error.rb +6 -9
- data/lib/janeway/functions/length.rb +1 -1
- data/lib/janeway/interpreter.rb +124 -19
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +41 -0
- data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +15 -10
- data/lib/janeway/interpreters/base.rb +26 -2
- data/lib/janeway/interpreters/binary_operator_interpreter.rb +10 -7
- data/lib/janeway/interpreters/child_segment_deleter.rb +19 -0
- data/lib/janeway/interpreters/child_segment_interpreter.rb +48 -8
- data/lib/janeway/interpreters/current_node_interpreter.rb +5 -3
- data/lib/janeway/interpreters/descendant_segment_interpreter.rb +13 -9
- data/lib/janeway/interpreters/filter_selector_deleter.rb +65 -0
- data/lib/janeway/interpreters/filter_selector_interpreter.rb +54 -14
- data/lib/janeway/interpreters/function_interpreter.rb +7 -5
- data/lib/janeway/interpreters/index_selector_deleter.rb +26 -0
- data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -2
- data/lib/janeway/interpreters/name_selector_deleter.rb +27 -0
- data/lib/janeway/interpreters/name_selector_interpreter.rb +19 -4
- data/lib/janeway/interpreters/root_node_deleter.rb +34 -0
- data/lib/janeway/interpreters/root_node_interpreter.rb +4 -2
- data/lib/janeway/interpreters/tree_constructor.rb +20 -1
- data/lib/janeway/interpreters/unary_operator_interpreter.rb +2 -2
- data/lib/janeway/interpreters/wildcard_selector_deleter.rb +32 -0
- data/lib/janeway/interpreters/wildcard_selector_interpreter.rb +26 -11
- data/lib/janeway/interpreters/yielder.rb +50 -12
- data/lib/janeway/lexer.rb +1 -1
- data/lib/janeway/normalized_path.rb +66 -0
- data/lib/janeway/parser.rb +3 -3
- data/lib/janeway/query.rb +70 -0
- data/lib/janeway/version.rb +1 -1
- data/lib/janeway.rb +16 -28
- metadata +12 -3
- data/lib/janeway/ast/query.rb +0 -98
data/lib/janeway/interpreter.rb
CHANGED
@@ -11,6 +11,7 @@ module Janeway
|
|
11
11
|
# It should be created for a single query and then discarded.
|
12
12
|
class Interpreter
|
13
13
|
attr_reader :jsonpath, :output
|
14
|
+
include Interpreters
|
14
15
|
|
15
16
|
# Interpret a query on the given input, return result
|
16
17
|
# @param input [Hash, Array]
|
@@ -21,25 +22,38 @@ module Janeway
|
|
21
22
|
new(ast).interpret(input)
|
22
23
|
end
|
23
24
|
|
24
|
-
# @param query [
|
25
|
-
def initialize(query)
|
26
|
-
raise ArgumentError, "expect
|
25
|
+
# @param query [Query] abstract syntax tree of the jsonpath query
|
26
|
+
def initialize(query, as: :finder, &block)
|
27
|
+
raise ArgumentError, "expect Query, got #{query.inspect}" unless query.is_a?(Query)
|
28
|
+
unless %i[finder iterator deleter].include?(as)
|
29
|
+
raise ArgumentError, "invalid interpreter type: #{as.inspect}"
|
30
|
+
end
|
27
31
|
|
28
32
|
@query = query
|
33
|
+
@type = as
|
29
34
|
@jsonpath = query.jsonpath
|
30
|
-
@
|
31
|
-
|
35
|
+
@root = query_to_interpreter_tree(@query, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return multiline JSON string describing the interpreter tree.
|
39
|
+
#
|
40
|
+
# This is not used for parsing / interpretation.
|
41
|
+
# It is intended to represent the interpreter tree to help with debugging.
|
42
|
+
# This format makes the tree structure much clearer than the #inspect output does.
|
43
|
+
#
|
44
|
+
# @return [String]
|
45
|
+
def to_json(options = {})
|
46
|
+
JSON.pretty_generate(@root.as_json, *options)
|
32
47
|
end
|
33
48
|
|
34
49
|
# @param input [Array, Hash] object to be searched
|
35
50
|
# @return [Object]
|
36
51
|
def interpret(input)
|
37
|
-
|
38
|
-
unless @input.is_a?(Hash) || @input.is_a?(Array)
|
52
|
+
unless input.is_a?(Hash) || input.is_a?(Array)
|
39
53
|
return [] # can't query on any other types, but need to check because a string is also valid json
|
40
54
|
end
|
41
55
|
|
42
|
-
@
|
56
|
+
@root.interpret(nil, nil, input, [])
|
43
57
|
rescue StandardError => e
|
44
58
|
# Error during interpretation. Convert it to a Janeway::Error and include the query in the message
|
45
59
|
error = err(e.message)
|
@@ -47,24 +61,57 @@ module Janeway
|
|
47
61
|
raise error
|
48
62
|
end
|
49
63
|
|
50
|
-
# Append an interpreter onto the end of the pipeline
|
51
|
-
# @param [Interpreters::Base]
|
52
|
-
def push(node)
|
53
|
-
@pipeline.last.next = node
|
54
|
-
end
|
55
|
-
|
56
64
|
private
|
57
65
|
|
66
|
+
# Build a tree of interpreter classes based on the query's abstract syntax tree.
|
67
|
+
#
|
68
|
+
# Child segments require special handling.
|
69
|
+
# See the detailed explanation at the end of this file.
|
70
|
+
#
|
58
71
|
# @return [Interpreters::RootNodeInterpreter]
|
59
|
-
def
|
60
|
-
|
72
|
+
def query_to_interpreter_tree(query, &block)
|
73
|
+
# Build an interpreter for each selector
|
74
|
+
interpreters =
|
61
75
|
query.node_list.map do |node|
|
62
76
|
Interpreters::TreeConstructor.ast_node_to_interpreter(node)
|
63
77
|
end
|
64
|
-
|
65
|
-
|
78
|
+
|
79
|
+
# Append iterotor / deleter as needed, to support #each, #delete operations
|
80
|
+
case @type
|
81
|
+
when :iterator then interpreters.push(Yielder.new(&block))
|
82
|
+
when :deleter then interpreters.push make_deleter(interpreters.pop)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Link interpreters together
|
86
|
+
interpreters.each_with_index do |node, i|
|
87
|
+
node.next = interpreters[i + 1]
|
66
88
|
end
|
67
|
-
|
89
|
+
|
90
|
+
# Child segments which contain multiple selectors are branches in the interpreter tree.
|
91
|
+
#
|
92
|
+
# For every child segment, remove all following interpreters and push
|
93
|
+
# them "inside" the child segment interpreter.
|
94
|
+
#
|
95
|
+
# Work backwards in case there are multiple child segments.
|
96
|
+
# For full explanation see the explanation at the end of this file.
|
97
|
+
selectors = []
|
98
|
+
interpreters.reverse_each do |node|
|
99
|
+
if node.is_a?(Interpreters::ChildSegmentInterpreter)
|
100
|
+
node.next = nil
|
101
|
+
node.push(selectors.pop) until selectors.empty?
|
102
|
+
selectors = [node]
|
103
|
+
else
|
104
|
+
selectors << node
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
selectors.last
|
109
|
+
end
|
110
|
+
|
111
|
+
# Make a Deleter that will delete the results matched by a Selector.
|
112
|
+
# @param interpreter [Interpreters::Base] interpeter subclass
|
113
|
+
def make_deleter(interpreter)
|
114
|
+
TreeConstructor.ast_node_to_deleter(interpreter.node)
|
68
115
|
end
|
69
116
|
|
70
117
|
# Return an Interpreter::Error with the specified message, include the query.
|
@@ -76,3 +123,61 @@ module Janeway
|
|
76
123
|
end
|
77
124
|
end
|
78
125
|
end
|
126
|
+
__END__
|
127
|
+
= Child Segment Handling
|
128
|
+
|
129
|
+
== Interpreter Tree Layout
|
130
|
+
|
131
|
+
The interpreter tree is a straight line of filter into filter, except for the case of child segments
|
132
|
+
(brackets) that contain multiple selectors, eg. "$.store['bicycle', 'book']".
|
133
|
+
|
134
|
+
Such child segments are branches in the tree.
|
135
|
+
|
136
|
+
In Janeway, child segments that contain only one selector are not represented
|
137
|
+
in the AST, only the selector itself is an AST node.
|
138
|
+
For the remainder of this explanation, the term 'child segment' refers to child
|
139
|
+
segments that contain multiple selectors.
|
140
|
+
|
141
|
+
== Child segment evaluation -- original approach and problem
|
142
|
+
|
143
|
+
The obvious approach to interpreting a child segment is to send the input to
|
144
|
+
each of the selectors, combine the resulting values, and forward that to then
|
145
|
+
'next' selector which follows the child segment.
|
146
|
+
This is how I originally implemented the Janeway interpeter.
|
147
|
+
|
148
|
+
That approach works fine when only values are being collected.
|
149
|
+
However Janeway now supports iteration with #each, so each interpretation
|
150
|
+
step carries along some new context information: the parent (object which
|
151
|
+
contains the current input value) and the path (list of array indices or hash
|
152
|
+
keys which define the path to the current input).
|
153
|
+
|
154
|
+
Interpreting a child segment by interpeting its selectors separately and combining the results
|
155
|
+
discards all of that context. Fully interpreting selectors only returns
|
156
|
+
values, not the parent or path data.
|
157
|
+
|
158
|
+
== Child segment evaluation -- new approach
|
159
|
+
|
160
|
+
Instead, embrace the idea of the child segment being a branch in the tree.
|
161
|
+
|
162
|
+
Take the chain of selectors which comes after the child segment, and copy that chain
|
163
|
+
onto the selectors inside the child segment.
|
164
|
+
|
165
|
+
Consider this jsonpath:
|
166
|
+
$.store['book', 'bicycle'].price
|
167
|
+
|
168
|
+
In the first approach, the interpretation tree has a diamond:
|
169
|
+
|
170
|
+
bicycle
|
171
|
+
$ store < > price
|
172
|
+
book
|
173
|
+
|
174
|
+
In the new approach, the interpretation tree splits into two separate branches
|
175
|
+
|
176
|
+
bicycle - price
|
177
|
+
$ store <
|
178
|
+
book - price
|
179
|
+
|
180
|
+
This way, the parent and path information is passed down normally to the following selectors.
|
181
|
+
|
182
|
+
When a Yielder is pushed onto the interpreter tree, copies of it must be pushed onto
|
183
|
+
the end of every branch.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Interprets array slice selector and deletes matching values
|
8
|
+
class ArraySliceSelectorDeleter < ArraySliceSelectorInterpreter
|
9
|
+
# Delete values at the indices matched by the array slice selector
|
10
|
+
#
|
11
|
+
# @param input [Array, Hash] the results of processing so far
|
12
|
+
# @param _parent [Array, Hash] parent of the input object
|
13
|
+
# @param _root [Array, Hash] the entire input
|
14
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
15
|
+
# @return [Array]
|
16
|
+
def interpret(input, _parent, _root, _path)
|
17
|
+
return [] unless input.is_a?(Array)
|
18
|
+
return [] if selector&.step&.zero? # RFC: When step is 0, no elements are selected.
|
19
|
+
|
20
|
+
# Calculate the upper and lower indices of the target range
|
21
|
+
lower = selector.lower_index(input.size)
|
22
|
+
upper = selector.upper_index(input.size)
|
23
|
+
|
24
|
+
# Convert bounds and step to index values.
|
25
|
+
# Omit the final index, since no value is collected for that.
|
26
|
+
# Delete indexes from largest to smallest, so that deleting an index does
|
27
|
+
# not change the remaining indexes
|
28
|
+
results = []
|
29
|
+
if selector.step.positive?
|
30
|
+
indexes = lower.step(to: upper - 1, by: selector.step).to_a
|
31
|
+
indexes.reverse_each { |i| results << input.delete_at(i) }
|
32
|
+
results.reverse
|
33
|
+
else
|
34
|
+
indexes = upper.step(to: lower + 1, by: selector.step).to_a
|
35
|
+
indexes.each { |i| results << input.delete_at(i) }
|
36
|
+
results
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -10,11 +10,12 @@ module Janeway
|
|
10
10
|
|
11
11
|
# Filter the input by applying the array slice selector.
|
12
12
|
#
|
13
|
-
# @param selector [ArraySliceSelector]
|
14
13
|
# @param input [Array, Hash] the results of processing so far
|
14
|
+
# @param _parent [Array, Hash] parent of the input object
|
15
15
|
# @param root [Array, Hash] the entire input
|
16
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
16
17
|
# @return [Array]
|
17
|
-
def interpret(input, root)
|
18
|
+
def interpret(input, _parent, root, path)
|
18
19
|
return [] unless input.is_a?(Array)
|
19
20
|
return [] if selector&.step&.zero? # RFC: When step is 0, no elements are selected.
|
20
21
|
|
@@ -22,23 +23,27 @@ module Janeway
|
|
22
23
|
lower = selector.lower_index(input.size)
|
23
24
|
upper = selector.upper_index(input.size)
|
24
25
|
|
25
|
-
# Collect
|
26
|
-
|
26
|
+
# Collect real index values. Omit the final index, since no value is collected for that.
|
27
|
+
indexes =
|
27
28
|
if selector.step.positive?
|
28
|
-
lower.step(to: upper - 1, by: selector.step).
|
29
|
+
lower.step(to: upper - 1, by: selector.step).to_a
|
29
30
|
else
|
30
|
-
upper.step(to: lower + 1, by: selector.step).
|
31
|
+
upper.step(to: lower + 1, by: selector.step).to_a
|
31
32
|
end
|
32
|
-
return
|
33
|
+
return indexes.map { |i| input[i] } unless @next
|
33
34
|
|
34
35
|
# Apply child selector to each node in the output node list
|
35
|
-
node_list = results
|
36
36
|
results = []
|
37
|
-
|
38
|
-
results.concat @next.interpret(
|
37
|
+
indexes.each do |i|
|
38
|
+
results.concat @next.interpret(input[i], input, root, path + [i])
|
39
39
|
end
|
40
40
|
results
|
41
41
|
end
|
42
|
+
|
43
|
+
# @return [Hash]
|
44
|
+
def as_json
|
45
|
+
{ type: type, value: node.to_s, next: @next&.as_json }.compact
|
46
|
+
end
|
42
47
|
end
|
43
48
|
end
|
44
49
|
end
|
@@ -33,11 +33,35 @@ module Janeway
|
|
33
33
|
|
34
34
|
# Interpret the input, return result or forward to next node.
|
35
35
|
#
|
36
|
-
# @param
|
37
|
-
# @param
|
36
|
+
# @param _input [Array, Hash] the results of processing so far
|
37
|
+
# @param _root [Array, Hash] the entire input
|
38
38
|
def interpret(_input, _root)
|
39
39
|
raise NotImplementedError, 'subclass must implement #interpret'
|
40
40
|
end
|
41
|
+
|
42
|
+
# @return [String]
|
43
|
+
def to_s
|
44
|
+
@node.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return hash representation of this selector interpreter
|
48
|
+
# @return [Hash]
|
49
|
+
def as_json
|
50
|
+
if node
|
51
|
+
{ type: type, value: node&.value, next: @next&.as_json }.compact
|
52
|
+
else
|
53
|
+
{ type: type, next: @next&.as_json }.compact
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [AST::Selector] AST node containing this interpreter's data
|
58
|
+
def selector
|
59
|
+
nil # subclass should implement
|
60
|
+
end
|
61
|
+
|
62
|
+
def type
|
63
|
+
self.class.to_s.split('::').last # eg. Janeway::AST::FunctionCall => "FunctionCall"
|
64
|
+
end
|
41
65
|
end
|
42
66
|
end
|
43
67
|
end
|
@@ -24,17 +24,20 @@ module Janeway
|
|
24
24
|
# This may contain literal values or nodes, whose value must be extracted before comparison.
|
25
25
|
#
|
26
26
|
# @param input [Array, Hash] the results of processing so far
|
27
|
+
# @param parent [Array, Hash] parent of the input object
|
27
28
|
# @param root [Array, Hash] the entire input
|
28
|
-
|
29
|
+
# @param _path [Array] ignored
|
30
|
+
def interpret(input, parent, root, _path = nil)
|
31
|
+
# FIXME: this branch could be pushed into the constructor, to convert O(n) work to O(1)
|
29
32
|
case operator.name
|
30
33
|
when :and, :or
|
31
34
|
# handle node list for existence check
|
32
|
-
lhs = @left.interpret(input, root)
|
33
|
-
rhs = @right.interpret(input, root)
|
35
|
+
lhs = @left.interpret(input, parent, root, [])
|
36
|
+
rhs = @right.interpret(input, parent, root, [])
|
34
37
|
when :equal, :not_equal, :less_than, :greater_than, :less_than_or_equal, :greater_than_or_equal
|
35
38
|
# handle node values for comparison check
|
36
|
-
lhs = to_single_value @left.interpret(input, root)
|
37
|
-
rhs = to_single_value @right.interpret(input, root)
|
39
|
+
lhs = to_single_value @left.interpret(input, parent, root, [])
|
40
|
+
rhs = to_single_value @right.interpret(input, parent, root, [])
|
38
41
|
else
|
39
42
|
raise "Don't know how to handle binary operator #{operator.name.inspect}"
|
40
43
|
end
|
@@ -54,8 +57,8 @@ module Janeway
|
|
54
57
|
# Any node list is guaranteed not to contain multiple values because the expression that
|
55
58
|
# produce it was already verified to be a singular query.
|
56
59
|
#
|
57
|
-
# @param
|
58
|
-
# @
|
60
|
+
# @param result [Object] node list, or single value of basic type
|
61
|
+
# @return [String, Integer, nil]
|
59
62
|
def to_single_value(result)
|
60
63
|
# Return basic types (ie. from AST::Number, AST::StringType, AST::Null)
|
61
64
|
return result unless result.is_a?(Array)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'child_segment_interpreter'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Child segment interpreter with selectors that delete matching elements
|
8
|
+
class ChildSegmentDeleter < ChildSegmentInterpreter
|
9
|
+
# @param child_segment [AST::ChildSegment]
|
10
|
+
def initialize(child_segment)
|
11
|
+
super
|
12
|
+
@selectors =
|
13
|
+
child_segment.map do |expr|
|
14
|
+
TreeConstructor.ast_node_to_deleter(expr)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -15,29 +15,69 @@ module Janeway
|
|
15
15
|
# @param child_segment [AST::ChildSegment]
|
16
16
|
def initialize(child_segment)
|
17
17
|
super
|
18
|
-
@
|
18
|
+
@selectors =
|
19
19
|
child_segment.map do |expr|
|
20
20
|
TreeConstructor.ast_node_to_interpreter(expr)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
# Append an interpreter onto each of the selector branches
|
25
|
+
# @param node [Interpreters::Base]
|
26
|
+
def push(node)
|
27
|
+
return unless node
|
28
|
+
|
29
|
+
node.next = nil
|
30
|
+
@selectors.each do |selector|
|
31
|
+
last_node = find_last(selector)
|
32
|
+
if last_node.is_a?(Interpreters::ChildSegmentInterpreter)
|
33
|
+
last_node.push(node.dup)
|
34
|
+
else
|
35
|
+
last_node.next = node.dup
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
24
40
|
# Interpret a set of 2 or more selectors, seperated by the union operator.
|
25
|
-
# All selectors are sent
|
41
|
+
# All selectors are sent identical input, the results are combined.
|
26
42
|
#
|
27
43
|
# @param input [Array, Hash] the results of processing so far
|
44
|
+
# @param parent [Array, Hash] parent of the input object
|
28
45
|
# @param root [Array, Hash] the entire input
|
46
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
29
47
|
# @return [Array]
|
30
|
-
def interpret(input, root)
|
48
|
+
def interpret(input, parent, root, path)
|
31
49
|
# Apply each expression to the input, collect results
|
32
50
|
results = []
|
33
|
-
@
|
34
|
-
results.concat
|
51
|
+
@selectors.each do |selector|
|
52
|
+
results.concat selector.interpret(input, parent, root, path)
|
35
53
|
end
|
36
54
|
|
37
|
-
|
38
|
-
|
55
|
+
results
|
56
|
+
end
|
39
57
|
|
40
|
-
|
58
|
+
# Return hash representation of this interpreter
|
59
|
+
# @return [Hash]
|
60
|
+
def as_json
|
61
|
+
{
|
62
|
+
type: type,
|
63
|
+
selectors: @selectors.map(&:as_json),
|
64
|
+
next: @next&.as_json,
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Find the last descendant of the given selector (ie. first one with no "next" selector)
|
71
|
+
# @param selector [Interpreters::Base]
|
72
|
+
# @return [Interpreters::Base]
|
73
|
+
def find_last(selector)
|
74
|
+
node = selector
|
75
|
+
loop do
|
76
|
+
break unless node.next
|
77
|
+
|
78
|
+
node = node.next
|
79
|
+
end
|
80
|
+
node
|
41
81
|
end
|
42
82
|
end
|
43
83
|
end
|
@@ -18,12 +18,14 @@ module Janeway
|
|
18
18
|
# If the selector did not match any node, the array may be empty.
|
19
19
|
# If there was no selector, then the current input node is returned in the array.
|
20
20
|
#
|
21
|
-
# @param
|
21
|
+
# @param input [Array, Hash] the results of processing so far
|
22
|
+
# @param parent [Array, Hash] parent of the input object
|
22
23
|
# @param root [Array, Hash] the entire input
|
24
|
+
# @param _path [Array] ignored
|
23
25
|
# @return [Array] Node List containing all results from evaluating this node's selectors.
|
24
|
-
def interpret(input, root)
|
26
|
+
def interpret(input, parent, root, _path)
|
25
27
|
if @next
|
26
|
-
@next.interpret(input, root)
|
28
|
+
@next.interpret(input, parent, root, [])
|
27
29
|
else
|
28
30
|
input
|
29
31
|
end
|
@@ -11,27 +11,31 @@ module Janeway
|
|
11
11
|
# Find all descendants of the current input that match the selector in the DescendantSegment
|
12
12
|
#
|
13
13
|
# @param input [Array, Hash] the results of processing so far
|
14
|
+
# @param parent [Array, Hash] parent of the input object
|
14
15
|
# @param root [Array, Hash] the entire input
|
16
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
15
17
|
# @return [Array<AST::Expression>] node list
|
16
|
-
def interpret(input, root)
|
17
|
-
visit(input) do |node|
|
18
|
-
@next.interpret(node, root)
|
18
|
+
def interpret(input, parent, root, path)
|
19
|
+
visit(input, parent, path) do |node, parent_of_node, sub_path|
|
20
|
+
@next.interpret(node, parent_of_node, root, sub_path)
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
24
|
# Visit all descendants of `input`.
|
23
25
|
# Return results of applying `action` on each.
|
24
|
-
|
25
|
-
|
26
|
+
# @param input [Array, Hash] the results of processing so far
|
27
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
28
|
+
# @param parent [Array, Hash] parent of the input object
|
29
|
+
def visit(input, parent, path, &block)
|
30
|
+
results = [yield(input, parent, path)]
|
26
31
|
|
27
32
|
case input
|
28
33
|
when Array
|
29
|
-
results.concat(input.map { |
|
34
|
+
results.concat(input.map.with_index { |value, i| visit(value, input, path + [i], &block) })
|
30
35
|
when Hash
|
31
|
-
results.concat(input.
|
32
|
-
else
|
33
|
-
input
|
36
|
+
results.concat(input.map { |key, value| visit(value, input, path + [key], &block) })
|
34
37
|
end
|
38
|
+
# basic types are ignored, they will be added by one of the above
|
35
39
|
|
36
40
|
results.flatten(1).compact
|
37
41
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Interprets a filter selector, and deletes matching values
|
8
|
+
class FilterSelectorDeleter < FilterSelectorInterpreter
|
9
|
+
# Interpret selector on the input.
|
10
|
+
# @param input [Hash] the results of processing so far
|
11
|
+
# @param root [Array, Hash] the entire input
|
12
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
13
|
+
def interpret_hash(input, root, _path)
|
14
|
+
# Apply filter expressions to the input data
|
15
|
+
results = []
|
16
|
+
input.each do |key, value|
|
17
|
+
# Run filter and interpret result
|
18
|
+
result = @expr.interpret(value, nil, root, [])
|
19
|
+
case result
|
20
|
+
when TrueClass then results << value # comparison test - pass
|
21
|
+
when FalseClass then next # comparison test - fail
|
22
|
+
when Array
|
23
|
+
next if result.empty?
|
24
|
+
|
25
|
+
results << value # existence test - node list
|
26
|
+
else
|
27
|
+
results << value # existence test. Null values here == success.
|
28
|
+
end
|
29
|
+
input.delete(key)
|
30
|
+
end
|
31
|
+
results
|
32
|
+
end
|
33
|
+
|
34
|
+
# Interpret selector on the input.
|
35
|
+
# @param input [Array] the results of processing so far
|
36
|
+
# @param root [Array, Hash] the entire input
|
37
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
38
|
+
def interpret_array(input, root, _path)
|
39
|
+
# Apply filter expressions to the input data
|
40
|
+
results = []
|
41
|
+
|
42
|
+
# Iterate in reverse order so that deletion does not alter the remaining indexes
|
43
|
+
i = input.size
|
44
|
+
input.reverse_each do |value|
|
45
|
+
i -= 1 # calculate array index
|
46
|
+
|
47
|
+
# Run filter and interpret result
|
48
|
+
result = @expr.interpret(value, nil, root, [])
|
49
|
+
case result
|
50
|
+
when TrueClass then results << value # comparison test - pass
|
51
|
+
when FalseClass then next # comparison test - fail
|
52
|
+
when Array
|
53
|
+
next if result.empty?
|
54
|
+
|
55
|
+
results << value # existence test - node list
|
56
|
+
else
|
57
|
+
results << value # existence test. Null values here == success.
|
58
|
+
end
|
59
|
+
input.delete_at(i)
|
60
|
+
end
|
61
|
+
results.reverse
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -27,37 +27,77 @@ module Janeway
|
|
27
27
|
|
28
28
|
# Interpret selector on the input.
|
29
29
|
# @param input [Array, Hash] the results of processing so far
|
30
|
+
# @param _parent [Array, Hash] parent of the input object
|
30
31
|
# @param root [Array, Hash] the entire input
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
33
|
+
def interpret(input, _parent, root, path)
|
34
|
+
case input
|
35
|
+
when Array then interpret_array(input, root, path)
|
36
|
+
when Hash then interpret_hash(input, root, path)
|
37
|
+
else [] # early exit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Interpret selector on the input.
|
42
|
+
# @param input [Hash] the results of processing so far
|
43
|
+
# @param root [Array, Hash] the entire input
|
44
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
45
|
+
def interpret_hash(input, root, path)
|
46
|
+
# Apply filter expressions to the input data
|
47
|
+
node_list = []
|
48
|
+
input.each do |key, value|
|
49
|
+
# Run filter and interpret result
|
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.
|
37
57
|
end
|
58
|
+
end
|
59
|
+
return node_list.map(&:last) unless @next
|
38
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
|
67
|
+
end
|
68
|
+
|
69
|
+
# Interpret selector on the input.
|
70
|
+
# @param input [Array] the results of processing so far
|
71
|
+
# @param root [Array, Hash] the entire input
|
72
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
73
|
+
def interpret_array(input, root, path)
|
39
74
|
# Apply filter expressions to the input data
|
40
75
|
node_list = []
|
41
|
-
|
76
|
+
input.each_with_index do |value, i|
|
42
77
|
# Run filter and interpret result
|
43
|
-
result = @expr.interpret(value, root)
|
78
|
+
result = @expr.interpret(value, nil, root, [])
|
44
79
|
case result
|
45
|
-
when TrueClass then node_list << value # comparison test - pass
|
80
|
+
when TrueClass then node_list << [i, value] # comparison test - pass
|
46
81
|
when FalseClass then nil # comparison test - fail
|
47
|
-
when Array then node_list << value unless result.empty? # existence test - node list
|
82
|
+
when Array then node_list << [i, value] unless result.empty? # existence test - node list
|
48
83
|
else
|
49
|
-
node_list << value # existence test. Null values here == success.
|
84
|
+
node_list << [i, value] # existence test. Null values here == success.
|
50
85
|
end
|
51
86
|
end
|
52
|
-
return node_list unless @next
|
87
|
+
return node_list.map(&:last) unless @next
|
53
88
|
|
54
89
|
# Apply child selector to each node in the output node list
|
55
90
|
results = []
|
56
|
-
node_list.each do |
|
57
|
-
results.concat @next.interpret(
|
91
|
+
node_list.each do |i, value|
|
92
|
+
results.concat @next.interpret(value, input, root, path + [i])
|
58
93
|
end
|
59
94
|
results
|
60
95
|
end
|
96
|
+
|
97
|
+
# @return [Hash]
|
98
|
+
def as_json
|
99
|
+
{ type: type, value: node.to_s, next: @next&.as_json }.compact
|
100
|
+
end
|
61
101
|
end
|
62
102
|
end
|
63
103
|
end
|