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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/bin/janeway +4 -2
- data/lib/janeway/ast/array_slice_selector.rb +6 -90
- data/lib/janeway/ast/binary_operator.rb +57 -51
- data/lib/janeway/ast/child_segment.rb +2 -5
- data/lib/janeway/ast/current_node.rb +2 -15
- data/lib/janeway/ast/descendant_segment.rb +0 -3
- data/lib/janeway/ast/expression.rb +34 -5
- data/lib/janeway/ast/function.rb +15 -10
- data/lib/janeway/ast/name_selector.rb +12 -7
- data/lib/janeway/ast/root_node.rb +2 -15
- data/lib/janeway/ast/selector.rb +5 -6
- data/lib/janeway/ast/string_type.rb +4 -0
- data/lib/janeway/ast/unary_operator.rb +12 -2
- data/lib/janeway/ast/wildcard_selector.rb +2 -2
- data/lib/janeway/enumerator.rb +2 -4
- data/lib/janeway/error.rb +30 -1
- data/lib/janeway/functions/count.rb +1 -8
- data/lib/janeway/functions/length.rb +1 -12
- data/lib/janeway/functions/match.rb +9 -18
- data/lib/janeway/functions/search.rb +11 -9
- data/lib/janeway/functions/value.rb +1 -7
- data/lib/janeway/functions.rb +130 -30
- data/lib/janeway/interpreter.rb +50 -31
- data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
- data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
- data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
- data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
- data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
- data/lib/janeway/interpreters/function_interpreter.rb +5 -1
- data/lib/janeway/interpreters/index_selector_delete_if.rb +0 -1
- data/lib/janeway/interpreters/iteration_helper.rb +19 -5
- data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
- data/lib/janeway/interpreters/tree_constructor.rb +0 -20
- data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
- data/lib/janeway/interpreters/yielder.rb +8 -2
- data/lib/janeway/iregexp.rb +402 -0
- data/lib/janeway/lexer.rb +64 -79
- data/lib/janeway/location.rb +3 -1
- data/lib/janeway/normalized_path.rb +11 -2
- data/lib/janeway/parser.rb +47 -35
- data/lib/janeway/query.rb +47 -2
- data/lib/janeway/token.rb +40 -5
- data/lib/janeway/version.rb +1 -1
- metadata +2 -8
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
- data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
- data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
- data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
- data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
- data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
- data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
|
@@ -21,23 +21,42 @@ module Janeway
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Visit all descendants of `input
|
|
25
|
-
#
|
|
24
|
+
# Visit all descendants of `input` and concatenate the results of
|
|
25
|
+
# `block` at each node. Iterative depth-first pre-order — a recursive
|
|
26
|
+
# implementation would risk SystemStackError on deep JSON and allocated
|
|
27
|
+
# an array-per-node plus a flatten pass.
|
|
28
|
+
#
|
|
26
29
|
# @param input [Array, Hash] the results of processing so far
|
|
27
|
-
# @param path [Array<String>] elements of normalized path to the current input
|
|
28
30
|
# @param parent [Array, Hash] parent of the input object
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
|
32
|
+
def visit(input, parent, path)
|
|
33
|
+
results = []
|
|
34
|
+
stack = [[input, parent, path]]
|
|
35
|
+
until stack.empty?
|
|
36
|
+
node, node_parent, node_path = stack.pop
|
|
37
|
+
results.concat(yield(node, node_parent, node_path))
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
case node
|
|
40
|
+
when Array
|
|
41
|
+
# Push in reverse so the leftmost child is popped first (pre-order).
|
|
42
|
+
i = node.size - 1
|
|
43
|
+
while i >= 0
|
|
44
|
+
stack.push([node[i], node, node_path + [i]])
|
|
45
|
+
i -= 1
|
|
46
|
+
end
|
|
47
|
+
when Hash
|
|
48
|
+
# Iterate to an array once so we can walk it in reverse.
|
|
49
|
+
pairs = node.to_a
|
|
50
|
+
i = pairs.size - 1
|
|
51
|
+
while i >= 0
|
|
52
|
+
k, v = pairs[i]
|
|
53
|
+
stack.push([v, node, node_path + [k]])
|
|
54
|
+
i -= 1
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
# Basic (non-container) types have no descendants — nothing to push.
|
|
37
58
|
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
results.flatten(1)
|
|
59
|
+
results
|
|
41
60
|
end
|
|
42
61
|
end
|
|
43
62
|
end
|
|
@@ -43,27 +43,9 @@ module Janeway
|
|
|
43
43
|
# @param root [Array, Hash] the entire input
|
|
44
44
|
# @param path [Array<String>] elements of normalized path to the current input
|
|
45
45
|
def interpret_hash(input, root, path)
|
|
46
|
-
# Apply filter expressions to the input data
|
|
47
46
|
node_list = []
|
|
48
|
-
input.each
|
|
49
|
-
|
|
50
|
-
result = @expr.interpret(value, nil, root, [])
|
|
51
|
-
case result
|
|
52
|
-
when TrueClass then node_list << [key, value] # comparison test - pass
|
|
53
|
-
when FalseClass then nil # comparison test - fail
|
|
54
|
-
when Array then node_list << [key, value] unless result.empty? # existence test - node list
|
|
55
|
-
else
|
|
56
|
-
node_list << [key, value] # existence test. Null values here == success.
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
return node_list.map(&:last) unless @next
|
|
60
|
-
|
|
61
|
-
# Apply child selector to each node in the output node list
|
|
62
|
-
results = []
|
|
63
|
-
node_list.each do |key, value|
|
|
64
|
-
results.concat @next.interpret(value, input, root, path + [key])
|
|
65
|
-
end
|
|
66
|
-
results
|
|
47
|
+
input.each { |key, value| node_list << [key, value] if filter_match?(value, root) }
|
|
48
|
+
forward_matches(node_list, input, root, path)
|
|
67
49
|
end
|
|
68
50
|
|
|
69
51
|
# Interpret selector on the input.
|
|
@@ -71,29 +53,39 @@ module Janeway
|
|
|
71
53
|
# @param root [Array, Hash] the entire input
|
|
72
54
|
# @param path [Array<String>] elements of normalized path to the current input
|
|
73
55
|
def interpret_array(input, root, path)
|
|
74
|
-
# Apply filter expressions to the input data
|
|
75
56
|
node_list = []
|
|
76
|
-
input.each_with_index
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
57
|
+
input.each_with_index { |value, i| node_list << [i, value] if filter_match?(value, root) }
|
|
58
|
+
forward_matches(node_list, input, root, path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Classify the result of running the filter expression against a value.
|
|
64
|
+
# Returns true if the value should be kept.
|
|
65
|
+
def filter_match?(value, root)
|
|
66
|
+
result = @expr.interpret(value, nil, root, [])
|
|
67
|
+
case result
|
|
68
|
+
when TrueClass then true # comparison test - pass
|
|
69
|
+
when FalseClass then false # comparison test - fail
|
|
70
|
+
when Array then !result.empty? # existence test - non-empty node list
|
|
71
|
+
else true # existence test - null / other values count as success
|
|
86
72
|
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Given a node_list of [key_or_index, value] pairs, apply @next to each
|
|
76
|
+
# and concat the results. If no @next, return just the values.
|
|
77
|
+
def forward_matches(node_list, input, root, path)
|
|
87
78
|
return node_list.map(&:last) unless @next
|
|
88
79
|
|
|
89
|
-
# Apply child selector to each node in the output node list
|
|
90
80
|
results = []
|
|
91
|
-
node_list.each do |
|
|
92
|
-
results.concat @next.interpret(value, input, root, path + [
|
|
81
|
+
node_list.each do |key, value|
|
|
82
|
+
results.concat @next.interpret(value, input, root, path + [key])
|
|
93
83
|
end
|
|
94
84
|
results
|
|
95
85
|
end
|
|
96
86
|
|
|
87
|
+
public
|
|
88
|
+
|
|
97
89
|
# @return [Hash]
|
|
98
90
|
def as_json
|
|
99
91
|
{ type: type, value: node.to_s, next: @next&.as_json }.compact
|
|
@@ -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
|
-
|
|
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|
|
|
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|
|
|
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|
|
|
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
|
-
#
|
|
41
|
-
|
|
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
|
-
#
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 [
|
|
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
|
-
|
|
38
|
+
EMPTY_RESULT
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
# Dummy method from Interpreters::Base, allow child segment interpreter to disable the
|