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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/janeway/ast/array_slice_selector.rb +6 -90
  4. data/lib/janeway/ast/binary_operator.rb +57 -51
  5. data/lib/janeway/ast/child_segment.rb +3 -5
  6. data/lib/janeway/ast/current_node.rb +2 -15
  7. data/lib/janeway/ast/descendant_segment.rb +0 -3
  8. data/lib/janeway/ast/expression.rb +35 -5
  9. data/lib/janeway/ast/function.rb +15 -10
  10. data/lib/janeway/ast/name_selector.rb +12 -7
  11. data/lib/janeway/ast/root_node.rb +2 -15
  12. data/lib/janeway/ast/selector.rb +5 -6
  13. data/lib/janeway/ast/unary_operator.rb +12 -2
  14. data/lib/janeway/ast/wildcard_selector.rb +2 -2
  15. data/lib/janeway/enumerator.rb +14 -7
  16. data/lib/janeway/error.rb +6 -1
  17. data/lib/janeway/functions/count.rb +1 -8
  18. data/lib/janeway/functions/length.rb +1 -12
  19. data/lib/janeway/functions/match.rb +3 -8
  20. data/lib/janeway/functions/search.rb +3 -8
  21. data/lib/janeway/functions/value.rb +1 -7
  22. data/lib/janeway/functions.rb +75 -2
  23. data/lib/janeway/interpreter.rb +50 -35
  24. data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
  25. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
  26. data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
  27. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
  28. data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
  29. data/lib/janeway/interpreters/function_interpreter.rb +5 -1
  30. data/lib/janeway/interpreters/index_selector_delete_if.rb +1 -0
  31. data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -1
  32. data/lib/janeway/interpreters/iteration_helper.rb +19 -5
  33. data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
  34. data/lib/janeway/interpreters/tree_constructor.rb +0 -20
  35. data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
  36. data/lib/janeway/interpreters/yielder.rb +8 -2
  37. data/lib/janeway/lexer.rb +66 -77
  38. data/lib/janeway/location.rb +3 -1
  39. data/lib/janeway/normalized_path.rb +23 -7
  40. data/lib/janeway/parser.rb +46 -34
  41. data/lib/janeway/query.rb +48 -5
  42. data/lib/janeway/token.rb +2 -5
  43. data/lib/janeway/version.rb +1 -1
  44. data/lib/janeway.rb +16 -3
  45. metadata +3 -10
  46. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
  47. data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
  48. data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
  49. data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
  50. data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
  51. data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
  52. 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
- super("Jsonpath query #{query} - #{msg}")
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
- # Define function body
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
- # Meaning of return value depends on the JSON type:
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
- AST::Function.new('match', parameters) do |str, str_iregexp|
56
- if str.is_a?(String) && str_iregexp.is_a?(String)
57
- regexp = translate_iregex_to_ruby_regex(str_iregexp)
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
- AST::Function.new('search', parameters) do |str, str_iregexp|
51
- if str.is_a?(String) && str_iregexp.is_a?(String)
52
- regexp = translate_iregex_to_ruby_regex(str_iregexp, anchor: false)
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) do |nodes|
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
@@ -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
@@ -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 StandardError => e
59
- # Error during interpretation. Convert it to a Janeway::Error and include the query in the message
60
- error = err(e.message)
61
- error.set_backtrace e.backtrace
62
- raise error
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
- # Child segments require special handling.
70
- # See the detailed explanation at the end of this file.
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
- # Build an interpreter for each selector
75
- interpreters =
76
- query.node_list.map do |node|
77
- Interpreters::TreeConstructor.ast_node_to_interpreter(node)
78
- end
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
- # Append iterotor / deleter as needed, to support #each, #delete operations
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 make_deleter(interpreters.pop)
84
- when :delete_if then interpreters.push make_delete_if(interpreters.pop, &block)
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
- # Link interpreters together
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
- # Child segments which contain multiple selectors are branches in the interpreter tree.
93
- #
94
- # For every child segment, remove all following interpreters and push
95
- # them "inside" the child segment interpreter.
96
- #
97
- # Work backwards in case there are multiple child segments.
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(selectors.pop) until selectors.empty?
104
- selectors = [node]
117
+ node.push(pending.pop) until pending.empty?
118
+ pending = [node]
105
119
  else
106
- selectors << node
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.ast_node_to_deleter(interpreter.node)
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 selector&.step&.zero? # RFC: When step is 0, no elements are selected.
30
+ return [] if effective_step.zero? # RFC: When step is 0, no elements are selected.
31
31
 
32
- # Calculate the upper and lower indices of the target range
33
- lower = selector.lower_index(input.size)
34
- upper = selector.upper_index(input.size)
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 selector.step.positive?
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 = upper.step(to: lower + 1, by: selector.step).to_a
51
- indexes.each { |i| results << input.delete_at(i) }
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 selector&.step&.zero? # RFC: When step is 0, no elements are selected.
21
-
22
- # Calculate the upper and lower indices of the target range
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
- # FIXME: this branch could be pushed into the constructor, to convert O(n) work to O(1)
32
- case operator.name
33
- when :and, :or
34
- # handle node list for existence check
35
- lhs = @left.interpret(input, parent, root, [])
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
- send(:"interpret_#{operator.name}", lhs, rhs)
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
- # Return results of applying `action` on each.
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
- def visit(input, parent, path, &block)
30
- results = [yield(input, parent, path)]
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
- case input
33
- when Array
34
- results.concat(input.map.with_index { |value, i| visit(value, input, path + [i], &block) })
35
- when Hash
36
- results.concat(input.map { |key, value| visit(value, input, path + [key], &block) })
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
- # basic types are ignored, they will be added by one of the above
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 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.
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 do |value, i|
77
- # Run filter and interpret result
78
- result = @expr.interpret(value, nil, root, [])
79
- case result
80
- when TrueClass then node_list << [i, value] # comparison test - pass
81
- when FalseClass then nil # comparison test - fail
82
- when Array then node_list << [i, value] unless result.empty? # existence test - node list
83
- else
84
- node_list << [i, value] # existence test. Null values here == success.
85
- end
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 |i, value|
92
- results.concat @next.interpret(value, input, root, path + [i])
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