janeway-jsonpath 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/bin/janeway +4 -2
  4. data/lib/janeway/ast/array_slice_selector.rb +6 -90
  5. data/lib/janeway/ast/binary_operator.rb +57 -51
  6. data/lib/janeway/ast/child_segment.rb +2 -5
  7. data/lib/janeway/ast/current_node.rb +2 -15
  8. data/lib/janeway/ast/descendant_segment.rb +0 -3
  9. data/lib/janeway/ast/expression.rb +34 -5
  10. data/lib/janeway/ast/function.rb +15 -10
  11. data/lib/janeway/ast/name_selector.rb +12 -7
  12. data/lib/janeway/ast/root_node.rb +2 -15
  13. data/lib/janeway/ast/selector.rb +5 -6
  14. data/lib/janeway/ast/string_type.rb +4 -0
  15. data/lib/janeway/ast/unary_operator.rb +12 -2
  16. data/lib/janeway/ast/wildcard_selector.rb +2 -2
  17. data/lib/janeway/enumerator.rb +2 -4
  18. data/lib/janeway/error.rb +30 -1
  19. data/lib/janeway/functions/count.rb +1 -8
  20. data/lib/janeway/functions/length.rb +1 -12
  21. data/lib/janeway/functions/match.rb +9 -18
  22. data/lib/janeway/functions/search.rb +11 -9
  23. data/lib/janeway/functions/value.rb +1 -7
  24. data/lib/janeway/functions.rb +130 -30
  25. data/lib/janeway/interpreter.rb +50 -31
  26. data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
  27. data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
  28. data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
  29. data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
  30. data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
  31. data/lib/janeway/interpreters/function_interpreter.rb +5 -1
  32. data/lib/janeway/interpreters/index_selector_delete_if.rb +0 -1
  33. data/lib/janeway/interpreters/iteration_helper.rb +19 -5
  34. data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
  35. data/lib/janeway/interpreters/tree_constructor.rb +0 -20
  36. data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
  37. data/lib/janeway/interpreters/yielder.rb +8 -2
  38. data/lib/janeway/iregexp.rb +402 -0
  39. data/lib/janeway/lexer.rb +64 -79
  40. data/lib/janeway/location.rb +3 -1
  41. data/lib/janeway/normalized_path.rb +11 -2
  42. data/lib/janeway/parser.rb +47 -35
  43. data/lib/janeway/query.rb +47 -2
  44. data/lib/janeway/token.rb +40 -5
  45. data/lib/janeway/version.rb +1 -1
  46. metadata +2 -8
  47. data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
  48. data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
  49. data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
  50. data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
  51. data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
  52. data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
  53. data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
@@ -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)
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
@@ -22,6 +22,10 @@ module Janeway
22
22
  def initialize(function)
23
23
  super
24
24
  @params = function.parameters.map { |param| TreeConstructor.ast_node_to_interpreter(param) }
25
+ # Build the function body from the registry. This is the point at
26
+ # which per-instance specialization happens — e.g. match/search
27
+ # compile a literal regex once, capturing it in the closure.
28
+ @body = Functions::REGISTRY.fetch(function.name)[:build].call(function.parameters)
25
29
  end
26
30
 
27
31
  # @param input [Array, Hash] the results of processing so far
@@ -30,7 +34,7 @@ module Janeway
30
34
  # @param _path [Array<String>] elements of normalized path to the current input
31
35
  def interpret(input, _parent, root, _path)
32
36
  params = interpret_function_parameters(@params, input, root)
33
- function.body.call(*params)
37
+ @body.call(*params)
34
38
  end
35
39
 
36
40
  # Evaluate the expressions in the parameter list to make the parameter values
@@ -31,7 +31,6 @@ module Janeway
31
31
  index += input.size if index.negative? # yield positive index for the normalize path
32
32
  return unless @yield_proc.call(input[index], input, path + [index])
33
33
 
34
- index += input.size if index.negative?
35
34
  input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
36
35
  [result]
37
36
  rescue IndexError
@@ -6,21 +6,28 @@ module Janeway
6
6
  module Interpreters
7
7
  # Mixin for interpreter classes that yield to a block
8
8
  module IterationHelper
9
+ # Sentinel block used by Interpreter#make_deleter to turn a DeleteIf
10
+ # into an unconditional deleter. `equal?`-comparable so future fast
11
+ # paths (bulk clear) can detect it.
12
+ PASS_ALL = proc { true }.freeze
9
13
  # Returns a Proc that yields the correct number of parameters to a block
10
14
  #
11
15
  # block.arity is -1 when no block is given, and an enumerator is being returned
12
16
  # @return [Proc] which takes 3 parameters
13
17
  def make_yield_proc(&block)
18
+ # Bind the block locally so the returned proc captures a local, not the
19
+ # containing instance's @block ivar (avoids an ivar read per yield).
20
+ b = block
14
21
  if block.arity.negative?
15
22
  # Yield just the value to an enumerator, to enable instance method calls on
16
23
  # matched values like this: enum.delete_if(&:even?)
17
- proc { |value, _parent, _path| @block.call(value) }
24
+ proc { |value, _parent, _path| b.call(value) }
18
25
  elsif block.arity > 3
19
26
  # Only do the work of constructing the normalized path when it is actually used
20
- proc { |value, parent, path| @block.call(value, parent, path.last, normalized_path(path)) }
27
+ proc { |value, parent, path| b.call(value, parent, path.last, normalized_path(path)) }
21
28
  else
22
29
  # block arity is 1, 2 or 3. Send all 3 parameters regardless.
23
- proc { |value, parent, path| @block.call(value, parent, path.last) }
30
+ proc { |value, parent, path| b.call(value, parent, path.last) }
24
31
  end
25
32
  end
26
33
 
@@ -37,8 +44,15 @@ module Janeway
37
44
  def normalized_path(components)
38
45
  # First component is the root identifer, the remaining components are
39
46
  # all index selectors or name selectors.
40
- # Handle the root identifier separately, because .normalize does not handle those.
41
- '$' + components[1..].map { NormalizedPath.normalize(_1) }.join
47
+ # Build the result string in a single pass — avoids `components[1..]`
48
+ # (slice allocation) plus `.map { ... }.join` (intermediate Array).
49
+ result = +'$'
50
+ i = 1
51
+ while i < components.size
52
+ result << NormalizedPath.normalize(components[i])
53
+ i += 1
54
+ end
55
+ result
42
56
  end
43
57
  end
44
58
  end
@@ -1,33 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'root_node_interpreter'
4
+ require_relative 'iteration_helper'
4
5
 
5
6
  module Janeway
6
7
  module Interpreters
7
- # Interprets the root node for deletion
8
- class RootNodeDeleter < RootNodeInterpreter
9
- # Delete all values from the root node.
10
- #
11
- # TODO: unclear, for deletion is there a difference between queries '$' and '$.*'?
8
+ # Delete values from the root node for which the block returns a truthy value.
9
+ class RootNodeDeleteIf < RootNodeInterpreter
10
+ include IterationHelper
11
+
12
+ # @param node [AST::RootNode]
13
+ def initialize(node, &block)
14
+ super(node)
15
+ @block = block
16
+
17
+ # Make a proc that yields the correct number of values to a block
18
+ @yield_proc = make_yield_proc(&block)
19
+ end
20
+
21
+ # Delete values from the root container for which the block returns truthy.
12
22
  #
13
23
  # @param _input [Array, Hash] the results of processing so far
14
24
  # @param _parent [Array, Hash] parent of the input object
15
25
  # @param root [Array, Hash] the entire input
16
26
  # @param _path [Array<String>] elements of normalized path to the current input
17
27
  # @return [Array] deleted elements
18
- def interpret(_input, _parent, root, _path)
28
+ def interpret(_input, _parent, root, _path = nil)
19
29
  case root
20
- when Array
21
- results = root.dup
22
- root.clear
23
- results
24
- when Hash
25
- results = root.values
26
- root.clear
27
- results
28
- else
29
- []
30
+ when Array then maybe_delete_array_values(root)
31
+ when Hash then maybe_delete_hash_values(root)
32
+ else []
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @param input [Array]
39
+ def maybe_delete_array_values(input)
40
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
41
+ if @block.equal?(IterationHelper::PASS_ALL)
42
+ results = input.dup
43
+ input.clear
44
+ return results
45
+ end
46
+
47
+ results = []
48
+ (input.size - 1).downto(0).each do |i|
49
+ next unless @yield_proc.call(input[i], input, ['$', i])
50
+
51
+ results << input.delete_at(i)
52
+ end
53
+ results.reverse
54
+ end
55
+
56
+ # @param input [Hash]
57
+ def maybe_delete_hash_values(input)
58
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
59
+ if @block.equal?(IterationHelper::PASS_ALL)
60
+ results = input.values
61
+ input.clear
62
+ return results
63
+ end
64
+
65
+ results = []
66
+ input.each do |key, value|
67
+ next unless @yield_proc.call(value, input, ['$', key])
68
+
69
+ results << input.delete(key)
30
70
  end
71
+ results
31
72
  end
32
73
  end
33
74
  end
@@ -44,26 +44,6 @@ module Janeway
44
44
  end
45
45
  end
46
46
 
47
- # Return a Deleter interpreter for the given AST node.
48
- # This interpreter deletes matched values.
49
- #
50
- # @param expr [AST::Expression]
51
- # @return [Interprteters::Base]
52
- def self.ast_node_to_deleter(expr)
53
- case expr
54
- when AST::IndexSelector then IndexSelectorDeleter.new(expr)
55
- when AST::ArraySliceSelector then ArraySliceSelectorDeleter.new(expr)
56
- when AST::NameSelector then NameSelectorDeleter.new(expr)
57
- when AST::FilterSelector then FilterSelectorDeleter.new(expr)
58
- when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
59
- when AST::ChildSegment then ChildSegmentDeleter.new(expr)
60
- when AST::RootNode then RootNodeDeleter.new(expr)
61
- when nil then nil # caller has no @next node
62
- else
63
- raise "Unknown AST expression: #{expr.inspect}"
64
- end
65
- end
66
-
67
47
  # Return a DeleteIf interpreter for the given AST node.
68
48
  # This interpreter deletes matched values, but only after
69
49
  # yielding to a block that returns a truthy value.
@@ -36,6 +36,15 @@ module Janeway
36
36
  # @param input [Array]
37
37
  # @param path [Array]
38
38
  def maybe_delete_array_values(input, path)
39
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
40
+ # Bulk clear is O(n) with much smaller constants than reverse-iterate
41
+ # + delete_at + per-element proc call.
42
+ if @block.equal?(IterationHelper::PASS_ALL)
43
+ results = input.dup
44
+ input.clear
45
+ return results
46
+ end
47
+
39
48
  results = []
40
49
  (input.size - 1).downto(0).each do |i|
41
50
  next unless @yield_proc.yield(input[i], input, path + [i])
@@ -48,6 +57,13 @@ module Janeway
48
57
  # @param input [Hash]
49
58
  # @param path [Array]
50
59
  def maybe_delete_hash_values(input, path)
60
+ # Fast path: unconditional delete (from Interpreter#make_deleter).
61
+ if @block.equal?(IterationHelper::PASS_ALL)
62
+ results = input.values
63
+ input.clear
64
+ return results
65
+ end
66
+
51
67
  results = []
52
68
  input.each do |key, value|
53
69
  next unless @yield_proc.yield(value, input, path + [key])
@@ -19,6 +19,12 @@ module Janeway
19
19
  @yield_proc = make_yield_proc(&block)
20
20
  end
21
21
 
22
+ # Frozen empty array — Yielder is only used by Enumerator#each and
23
+ # #find_paths, both of which discard the traversal's return value
24
+ # (results are surfaced via yield instead). Returning a shared frozen
25
+ # array avoids allocating a wrapper array per leaf.
26
+ EMPTY_RESULT = [].freeze
27
+
22
28
  # Yield each input value
23
29
  #
24
30
  # @param input [Array, Hash] the results of processing so far
@@ -26,10 +32,10 @@ module Janeway
26
32
  # @param _root [Array, Hash] the entire input
27
33
  # @param path [Array<String, Integer>] components of normalized path to the current input
28
34
  # @yieldparam [Object] matched value
29
- # @return [Object] input as node list
35
+ # @return [Array] frozen empty array — see EMPTY_RESULT
30
36
  def interpret(input, parent, _root, path)
31
37
  @yield_proc.call(input, parent, path)
32
- input.is_a?(Array) ? input : [input]
38
+ EMPTY_RESULT
33
39
  end
34
40
 
35
41
  # Dummy method from Interpreters::Base, allow child segment interpreter to disable the