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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +201 -28
  3. data/bin/janeway +36 -9
  4. data/lib/janeway/ast/expression.rb +1 -2
  5. data/lib/janeway/enumerator.rb +43 -0
  6. data/lib/janeway/error.rb +6 -9
  7. data/lib/janeway/functions/length.rb +1 -1
  8. data/lib/janeway/interpreter.rb +124 -19
  9. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +41 -0
  10. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +15 -10
  11. data/lib/janeway/interpreters/base.rb +26 -2
  12. data/lib/janeway/interpreters/binary_operator_interpreter.rb +10 -7
  13. data/lib/janeway/interpreters/child_segment_deleter.rb +19 -0
  14. data/lib/janeway/interpreters/child_segment_interpreter.rb +48 -8
  15. data/lib/janeway/interpreters/current_node_interpreter.rb +5 -3
  16. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +13 -9
  17. data/lib/janeway/interpreters/filter_selector_deleter.rb +65 -0
  18. data/lib/janeway/interpreters/filter_selector_interpreter.rb +54 -14
  19. data/lib/janeway/interpreters/function_interpreter.rb +7 -5
  20. data/lib/janeway/interpreters/index_selector_deleter.rb +26 -0
  21. data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -2
  22. data/lib/janeway/interpreters/name_selector_deleter.rb +27 -0
  23. data/lib/janeway/interpreters/name_selector_interpreter.rb +19 -4
  24. data/lib/janeway/interpreters/root_node_deleter.rb +34 -0
  25. data/lib/janeway/interpreters/root_node_interpreter.rb +4 -2
  26. data/lib/janeway/interpreters/tree_constructor.rb +20 -1
  27. data/lib/janeway/interpreters/unary_operator_interpreter.rb +2 -2
  28. data/lib/janeway/interpreters/wildcard_selector_deleter.rb +32 -0
  29. data/lib/janeway/interpreters/wildcard_selector_interpreter.rb +26 -11
  30. data/lib/janeway/interpreters/yielder.rb +50 -12
  31. data/lib/janeway/lexer.rb +1 -1
  32. data/lib/janeway/normalized_path.rb +66 -0
  33. data/lib/janeway/parser.rb +3 -3
  34. data/lib/janeway/query.rb +70 -0
  35. data/lib/janeway/version.rb +1 -1
  36. data/lib/janeway.rb +16 -28
  37. metadata +12 -3
  38. data/lib/janeway/ast/query.rb +0 -98
@@ -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 [AST::Query] abstract syntax tree of the jsonpath query
25
- def initialize(query)
26
- raise ArgumentError, "expect AST::Query, got #{query.inspect}" unless query.is_a?(AST::Query)
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
- @input = nil
31
- @pipeline = query_to_interpreter_pipeline(@query)
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
- @input = input
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
- @pipeline.first.interpret(nil, input)
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 query_to_interpreter_pipeline(query)
60
- pipeline =
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
- pipeline.each_with_index do |node, i|
65
- node.next = pipeline[i + 1]
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
- pipeline
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 values from target indices. Omit the value from the final index.
26
- results =
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).map { input[_1] }
29
+ lower.step(to: upper - 1, by: selector.step).to_a
29
30
  else
30
- upper.step(to: lower + 1, by: selector.step).map { input[_1] }
31
+ upper.step(to: lower + 1, by: selector.step).to_a
31
32
  end
32
- return results unless @next
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
- node_list.each do |node|
38
- results.concat @next.interpret(node, root)
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 input [Array, Hash] the results of processing so far
37
- # @param root [Array, Hash] the entire input
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
- def interpret(input, root)
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 node [AST::Expression]
58
- # @param input [Object]
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
- @nodes =
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 the identical input, the results are combined.
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
- @nodes.each do |node|
34
- results.concat node.interpret(input, root)
51
+ @selectors.each do |selector|
52
+ results.concat selector.interpret(input, parent, root, path)
35
53
  end
36
54
 
37
- # Return results, or forward them to the next selector
38
- return results unless @next
55
+ results
56
+ end
39
57
 
40
- @next.interpret(results, root)
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 _input [Array, Hash] the results of processing so far
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
- def visit(input, &action)
25
- results = [yield(input)]
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 { |elt| visit(elt, &action) })
34
+ results.concat(input.map.with_index { |value, i| visit(value, input, path + [i], &block) })
30
35
  when Hash
31
- results.concat(input.values.map { |value| visit(value, &action) })
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
- def interpret(input, root)
32
- values =
33
- case input
34
- when Array then input
35
- when Hash then input.values
36
- else return [] # early exit
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
- values.each do |value|
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 |node|
57
- results.concat @next.interpret(node, root)
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