janeway-jsonpath 0.5.0 → 0.6.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 +83 -5
- data/bin/janeway +77 -28
- data/lib/janeway/ast/child_segment.rb +8 -7
- data/lib/janeway/ast/filter_selector.rb +0 -2
- data/lib/janeway/ast/function.rb +2 -0
- data/lib/janeway/ast/selector.rb +17 -18
- data/lib/janeway/ast.rb +31 -0
- data/lib/janeway/enumerator.rb +152 -5
- data/lib/janeway/interpreter.rb +11 -1
- data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +57 -0
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +1 -1
- data/lib/janeway/interpreters/child_segment_delete_if.rb +20 -0
- data/lib/janeway/interpreters/child_segment_deleter.rb +1 -1
- data/lib/janeway/interpreters/filter_selector_delete_if.rb +73 -0
- data/lib/janeway/interpreters/index_selector_delete_if.rb +40 -0
- data/lib/janeway/interpreters/iteration_helper.rb +45 -0
- data/lib/janeway/interpreters/name_selector_delete_if.rb +42 -0
- data/lib/janeway/interpreters/root_node_delete_if.rb +34 -0
- data/lib/janeway/interpreters/tree_constructor.rb +31 -1
- data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +61 -0
- data/lib/janeway/interpreters/yielder.rb +7 -33
- data/lib/janeway/lexer.rb +40 -40
- data/lib/janeway/parser.rb +2 -0
- data/lib/janeway/query.rb +40 -0
- data/lib/janeway/version.rb +2 -1
- data/lib/janeway.rb +50 -37
- metadata +11 -2
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'array_slice_selector_interpreter'
|
4
|
+
require_relative 'iteration_helper'
|
5
|
+
|
6
|
+
module Janeway
|
7
|
+
module Interpreters
|
8
|
+
# Delete values that match the array slice selector and yield true from the block
|
9
|
+
class ArraySliceSelectorDeleteIf < ArraySliceSelectorInterpreter
|
10
|
+
include IterationHelper
|
11
|
+
|
12
|
+
# @param node [AST::Expression]
|
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 at the indices matched by the array slice selector
|
22
|
+
#
|
23
|
+
# @param input [Array, Hash] the results of processing so far
|
24
|
+
# @param _parent [Array, Hash] parent of the input object
|
25
|
+
# @param _root [Array, Hash] the entire input
|
26
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
27
|
+
# @return [Array]
|
28
|
+
def interpret(input, _parent, _root, path)
|
29
|
+
return [] unless input.is_a?(Array)
|
30
|
+
return [] if selector&.step&.zero? # RFC: When step is 0, no elements are selected.
|
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
|
40
|
+
results = []
|
41
|
+
if selector.step.positive?
|
42
|
+
indexes = lower.step(to: upper - 1, by: selector.step).to_a
|
43
|
+
indexes.reverse_each do |i|
|
44
|
+
next unless @yield_proc.call(input[i], input, path + [i])
|
45
|
+
|
46
|
+
results << input.delete_at(i)
|
47
|
+
end
|
48
|
+
results.reverse
|
49
|
+
else
|
50
|
+
indexes = upper.step(to: lower + 1, by: selector.step).to_a
|
51
|
+
indexes.each { |i| results << input.delete_at(i) }
|
52
|
+
results
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -6,7 +6,7 @@ module Janeway
|
|
6
6
|
module Interpreters
|
7
7
|
# Interprets array slice selector and deletes matching values
|
8
8
|
class ArraySliceSelectorDeleter < ArraySliceSelectorInterpreter
|
9
|
-
# Delete values
|
9
|
+
# Delete values matched by the array slice selector
|
10
10
|
#
|
11
11
|
# @param input [Array, Hash] the results of processing so far
|
12
12
|
# @param _parent [Array, Hash] parent of the input object
|
@@ -0,0 +1,20 @@
|
|
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 if
|
8
|
+
# the given block yields a truthy value
|
9
|
+
class ChildSegmentDeleteIf < ChildSegmentInterpreter
|
10
|
+
# @param child_segment [AST::ChildSegment]
|
11
|
+
def initialize(child_segment, &block)
|
12
|
+
super(child_segment)
|
13
|
+
@selectors =
|
14
|
+
child_segment.map do |expr|
|
15
|
+
TreeConstructor.ast_node_to_delete_if(expr, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,7 +5,7 @@ require_relative 'child_segment_interpreter'
|
|
5
5
|
module Janeway
|
6
6
|
module Interpreters
|
7
7
|
# Child segment interpreter with selectors that delete matching elements
|
8
|
-
class ChildSegmentDeleter < ChildSegmentInterpreter
|
8
|
+
class ChildSegmentDeleter < ChildSegmentInterpreter
|
9
9
|
# @param child_segment [AST::ChildSegment]
|
10
10
|
def initialize(child_segment)
|
11
11
|
super
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'filter_selector_interpreter'
|
4
|
+
require_relative 'iteration_helper'
|
5
|
+
|
6
|
+
module Janeway
|
7
|
+
module Interpreters
|
8
|
+
# Interprets a filter selector by deleting matching values for which the block returns a truthy value
|
9
|
+
class FilterSelectorDeleteIf < FilterSelectorInterpreter
|
10
|
+
include IterationHelper
|
11
|
+
|
12
|
+
# @param selector [AST::FilterSelector]
|
13
|
+
def initialize(selector, &block)
|
14
|
+
super(selector)
|
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
|
+
# Interpret selector on the input.
|
22
|
+
# @param input [Hash] the results of processing so far
|
23
|
+
# @param root [Array, Hash] the entire input
|
24
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
25
|
+
def interpret_hash(input, root, path)
|
26
|
+
# Apply filter expressions to the input data
|
27
|
+
results = []
|
28
|
+
input.each do |key, value|
|
29
|
+
# Run filter and interpret result
|
30
|
+
result = @expr.interpret(value, nil, root, [])
|
31
|
+
case result
|
32
|
+
when FalseClass then next # comparison test - fail
|
33
|
+
when Array then next if result.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
# filter test passed, next yield value to block
|
37
|
+
next unless @yield_proc.call(value, input, path + [key])
|
38
|
+
|
39
|
+
results << input.delete(key)
|
40
|
+
end
|
41
|
+
results
|
42
|
+
end
|
43
|
+
|
44
|
+
# Interpret selector on the input.
|
45
|
+
# @param input [Array] the results of processing so far
|
46
|
+
# @param root [Array, Hash] the entire input
|
47
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
48
|
+
def interpret_array(input, root, path)
|
49
|
+
# Apply filter expressions to the input data
|
50
|
+
results = []
|
51
|
+
|
52
|
+
# Iterate in reverse order so that deletion does not alter the remaining indexes
|
53
|
+
i = input.size
|
54
|
+
input.reverse_each do |value|
|
55
|
+
i -= 1 # calculate array index
|
56
|
+
|
57
|
+
# Run filter and interpret result
|
58
|
+
result = @expr.interpret(value, nil, root, [])
|
59
|
+
case result
|
60
|
+
when FalseClass then next # comparison test - fail
|
61
|
+
when Array then next if result.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
# filter test passed, next yield value to block
|
65
|
+
next unless @yield_proc.call(value, input, path + [i])
|
66
|
+
|
67
|
+
results << input.delete_at(i)
|
68
|
+
end
|
69
|
+
results.reverse
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'index_selector_interpreter'
|
4
|
+
require_relative 'iteration_helper'
|
5
|
+
|
6
|
+
module Janeway
|
7
|
+
module Interpreters
|
8
|
+
# Interprets an index selector, and deletes the matched value if the block returns true.
|
9
|
+
class IndexSelectorDeleteIf < IndexSelectorInterpreter
|
10
|
+
include IterationHelper
|
11
|
+
|
12
|
+
# @param selector [AST::IndexSelector]
|
13
|
+
def initialize(selector, &block)
|
14
|
+
super(selector)
|
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
|
+
# Interpret selector on the given input.
|
22
|
+
# @param input [Array, Hash] the results of processing so far
|
23
|
+
# @param _parent [Array, Hash] parent of the input object
|
24
|
+
# @param _root [Array, Hash] the entire input
|
25
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
26
|
+
def interpret(input, _parent, _root, path)
|
27
|
+
return [] unless input.is_a?(Array)
|
28
|
+
|
29
|
+
index = selector.value
|
30
|
+
result = input.fetch(index) # raises IndexError if no such index
|
31
|
+
return unless @yield_proc.call(input[index], input, path + [index])
|
32
|
+
|
33
|
+
input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
|
34
|
+
[result]
|
35
|
+
rescue IndexError
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../normalized_path'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Mixin for interpreter classes that yield to a block
|
8
|
+
module IterationHelper
|
9
|
+
# Returns a Proc that yields the correct number of parameters to a block
|
10
|
+
#
|
11
|
+
# block.arity is -1 when no block is given, and an enumerator is being returned
|
12
|
+
# @return [Proc] which takes 3 parameters
|
13
|
+
def make_yield_proc(&block)
|
14
|
+
if block.arity.negative?
|
15
|
+
# Yield just the value to an enumerator, to enable instance method calls on
|
16
|
+
# matched values like this: enum.delete_if(&:even?)
|
17
|
+
proc { |value, _parent, _path| @block.call(value) }
|
18
|
+
elsif block.arity > 3
|
19
|
+
# 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)) }
|
21
|
+
else
|
22
|
+
# block arity is 1, 2 or 3. Send all 3 parameters regardless.
|
23
|
+
proc { |value, parent, path| @block.call(value, parent, path.last) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convert the list of path elements into a normalized query string.
|
28
|
+
#
|
29
|
+
# This form uses a subset of jsonpath that unambiguously points to a value
|
30
|
+
# using only name and index selectors.
|
31
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
|
32
|
+
#
|
33
|
+
# Name selectors must use bracket notation, not shorthand.
|
34
|
+
#
|
35
|
+
# @param components [Array<String, Integer>]
|
36
|
+
# @return [String]
|
37
|
+
def normalized_path(components)
|
38
|
+
# First component is the root identifer, the remaining components are
|
39
|
+
# 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
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'name_selector_interpreter'
|
4
|
+
require_relative 'iteration_helper'
|
5
|
+
|
6
|
+
module Janeway
|
7
|
+
module Interpreters
|
8
|
+
# Interprets a name selector, and deletes the matched values if the block returns a truthy value.
|
9
|
+
class NameSelectorDeleteIf < NameSelectorInterpreter
|
10
|
+
include IterationHelper
|
11
|
+
|
12
|
+
# @param selector [AST::NameSelector]
|
13
|
+
def initialize(selector, &block)
|
14
|
+
super(selector)
|
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
|
+
# Interpret selector on the given input.
|
22
|
+
# Delete matching value for which the block returns truthy value.
|
23
|
+
#
|
24
|
+
# @param input [Array, Hash] the results of processing so far
|
25
|
+
# @param _parent [Array, Hash] parent of the input object
|
26
|
+
# @param _root [Array, Hash] the entire input
|
27
|
+
# @param path [Array] elements of normalized path to the current input
|
28
|
+
def interpret(input, _parent, _root, path)
|
29
|
+
return [] unless input.is_a?(Hash) && input.key?(name)
|
30
|
+
return [] unless @yield_proc.call(input[name], input, path + [name])
|
31
|
+
|
32
|
+
[input.delete(name)]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return hash representation of this interpreter
|
36
|
+
# @return [Hash]
|
37
|
+
def as_json
|
38
|
+
{ type: type, value: name }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'root_node_interpreter'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
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 '$.*'?
|
12
|
+
#
|
13
|
+
# @param _input [Array, Hash] the results of processing so far
|
14
|
+
# @param _parent [Array, Hash] parent of the input object
|
15
|
+
# @param root [Array, Hash] the entire input
|
16
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
17
|
+
# @return [Array] deleted elements
|
18
|
+
def interpret(_input, _parent, root, _path)
|
19
|
+
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
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -17,6 +17,12 @@ module Janeway
|
|
17
17
|
alias_method :node, :interpret
|
18
18
|
end
|
19
19
|
|
20
|
+
# Return an interpreter for the given AST node.
|
21
|
+
# This interpreter forwards matcheed values to the next interpreter, or
|
22
|
+
# returns them if there is no next interpreter.
|
23
|
+
#
|
24
|
+
# @param expr [AST::Expression]
|
25
|
+
# @return [Interprteters::Base]
|
20
26
|
def self.ast_node_to_interpreter(expr)
|
21
27
|
case expr
|
22
28
|
when AST::RootNode then Interpreters::RootNodeInterpreter.new(expr)
|
@@ -38,6 +44,11 @@ module Janeway
|
|
38
44
|
end
|
39
45
|
end
|
40
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]
|
41
52
|
def self.ast_node_to_deleter(expr)
|
42
53
|
case expr
|
43
54
|
when AST::IndexSelector then IndexSelectorDeleter.new(expr)
|
@@ -47,12 +58,31 @@ module Janeway
|
|
47
58
|
when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
|
48
59
|
when AST::ChildSegment then ChildSegmentDeleter.new(expr)
|
49
60
|
when AST::RootNode then RootNodeDeleter.new(expr)
|
50
|
-
|
51
61
|
when nil then nil # caller has no @next node
|
52
62
|
else
|
53
63
|
raise "Unknown AST expression: #{expr.inspect}"
|
54
64
|
end
|
55
65
|
end
|
66
|
+
|
67
|
+
# Return a DeleteIf interpreter for the given AST node.
|
68
|
+
# This interpreter deletes matched values, but only after
|
69
|
+
# yielding to a block that returns a truthy value.
|
70
|
+
#
|
71
|
+
# @param expr [AST::Expression]
|
72
|
+
# @return [Interprteters::Base]
|
73
|
+
def self.ast_node_to_delete_if(expr, &block)
|
74
|
+
case expr
|
75
|
+
when AST::IndexSelector then IndexSelectorDeleteIf.new(expr, &block)
|
76
|
+
when AST::ArraySliceSelector then ArraySliceSelectorDeleteIf.new(expr, &block)
|
77
|
+
when AST::NameSelector then NameSelectorDeleteIf.new(expr, &block)
|
78
|
+
when AST::FilterSelector then FilterSelectorDeleteIf.new(expr, &block)
|
79
|
+
when AST::WildcardSelector then WildcardSelectorDeleteIf.new(expr, &block)
|
80
|
+
when AST::ChildSegment then ChildSegmentDeleteIf.new(expr, &block)
|
81
|
+
when AST::RootNode then RootNodeDeleteIf.new(expr, &block)
|
82
|
+
else
|
83
|
+
raise "Unknown AST expression: #{expr.inspect}"
|
84
|
+
end
|
85
|
+
end
|
56
86
|
end
|
57
87
|
end
|
58
88
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'wildcard_selector_interpreter'
|
4
|
+
require_relative 'iteration_helper'
|
5
|
+
|
6
|
+
module Janeway
|
7
|
+
module Interpreters
|
8
|
+
# Interprets a wildcard selector by deleting array / hash values for which a block returns true.
|
9
|
+
class WildcardSelectorDeleteIf < WildcardSelectorInterpreter
|
10
|
+
include IterationHelper
|
11
|
+
|
12
|
+
# @param selector [AST::WildcardSelector]
|
13
|
+
def initialize(selector, &block)
|
14
|
+
super(selector)
|
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 all elements from the input
|
22
|
+
#
|
23
|
+
# @param input [Array, Hash] the results of processing so far
|
24
|
+
# @param _parent [Array, Hash] parent of the input object
|
25
|
+
# @param _root [Array, Hash] the entire input
|
26
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
27
|
+
# @return [Array] deleted elements
|
28
|
+
def interpret(input, _parent, _root, path)
|
29
|
+
case input
|
30
|
+
when Array then maybe_delete_array_values(input, path)
|
31
|
+
when Hash then maybe_delete_hash_values(input, path)
|
32
|
+
else []
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param input [Array]
|
37
|
+
# @param path [Array]
|
38
|
+
def maybe_delete_array_values(input, path)
|
39
|
+
results = []
|
40
|
+
(input.size - 1).downto(0).each do |i|
|
41
|
+
next unless @yield_proc.yield(input[i], input, path + [i])
|
42
|
+
|
43
|
+
results << input.delete_at(i)
|
44
|
+
end
|
45
|
+
results.reverse
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param input [Hash]
|
49
|
+
# @param path [Array]
|
50
|
+
def maybe_delete_hash_values(input, path)
|
51
|
+
results = []
|
52
|
+
input.each do |key, value|
|
53
|
+
next unless @yield_proc.yield(value, input, path + [key])
|
54
|
+
|
55
|
+
results << input.delete(key)
|
56
|
+
end
|
57
|
+
results
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,31 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'base'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'iteration_helper'
|
5
5
|
|
6
6
|
module Janeway
|
7
7
|
module Interpreters
|
8
8
|
# Yields each input value.
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# This is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
|
11
11
|
# This is a supporting class for the Janeway.each method.
|
12
12
|
class Yielder
|
13
|
+
include IterationHelper
|
14
|
+
|
13
15
|
def initialize(&block)
|
14
16
|
@block = block
|
15
17
|
|
16
|
-
#
|
17
|
-
|
18
|
-
@yield_to_block =
|
19
|
-
if block.arity.negative?
|
20
|
-
# Yield values only to an enumerator.
|
21
|
-
proc { |value, _parent, _path| @block.call(value) }
|
22
|
-
elsif block.arity > 3
|
23
|
-
# Only do the work of constructing the normalized path when it is actually used
|
24
|
-
proc { |value, parent, path| @block.call(value, parent, path.last, normalized_path(path)) }
|
25
|
-
else
|
26
|
-
# block arity is 1, 2 or 3. Send all 3.
|
27
|
-
proc { |value, parent, path| @block.call(value, parent, path.last) }
|
28
|
-
end
|
18
|
+
# Make a proc that yields the correct number of values to a block
|
19
|
+
@yield_proc = make_yield_proc(&block)
|
29
20
|
end
|
30
21
|
|
31
22
|
# Yield each input value
|
@@ -37,27 +28,10 @@ module Janeway
|
|
37
28
|
# @yieldparam [Object] matched value
|
38
29
|
# @return [Object] input as node list
|
39
30
|
def interpret(input, parent, _root, path)
|
40
|
-
@
|
31
|
+
@yield_proc.call(input, parent, path)
|
41
32
|
input.is_a?(Array) ? input : [input]
|
42
33
|
end
|
43
34
|
|
44
|
-
# Convert the list of path elements into a normalized query string.
|
45
|
-
#
|
46
|
-
# This form uses a subset of jsonpath that unambiguously points to a value
|
47
|
-
# using only name and index selectors.
|
48
|
-
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
|
49
|
-
#
|
50
|
-
# Name selectors must use bracket notation, not shorthand.
|
51
|
-
#
|
52
|
-
# @param components [Array<String, Integer>]
|
53
|
-
# @return [String]
|
54
|
-
def normalized_path(components)
|
55
|
-
# First component is the root identifer, the remaining components are
|
56
|
-
# all index selectors or name selectors.
|
57
|
-
# Handle the root identifier separately, because .normalize does not handle those.
|
58
|
-
'$' + components[1..].map { NormalizedPath.normalize(_1) }.join
|
59
|
-
end
|
60
|
-
|
61
35
|
# Dummy method from Interpreters::Base, allow child segment interpreter to disable the
|
62
36
|
# non-exist 'next' link.
|
63
37
|
# @return [void]
|
data/lib/janeway/lexer.rb
CHANGED
@@ -5,50 +5,50 @@ require_relative 'token'
|
|
5
5
|
require_relative 'error'
|
6
6
|
|
7
7
|
module Janeway
|
8
|
-
OPERATORS = {
|
9
|
-
and: '&&',
|
10
|
-
array_slice_separator: ':',
|
11
|
-
child_end: ']',
|
12
|
-
child_start: '[',
|
13
|
-
current_node: '@',
|
14
|
-
descendants: '..',
|
15
|
-
dot: '.',
|
16
|
-
equal: '==',
|
17
|
-
filter: '?',
|
18
|
-
greater_than: '>',
|
19
|
-
greater_than_or_equal: '>=',
|
20
|
-
group_end: ')',
|
21
|
-
group_start: '(',
|
22
|
-
less_than: '<',
|
23
|
-
less_than_or_equal: '<=',
|
24
|
-
minus: '-',
|
25
|
-
not: '!',
|
26
|
-
not_equal: '!=',
|
27
|
-
or: '||',
|
28
|
-
root: '$',
|
29
|
-
union: ',',
|
30
|
-
wildcard: '*',
|
31
|
-
}.freeze
|
32
|
-
ONE_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 1 }.freeze
|
33
|
-
TWO_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 2 }.freeze
|
34
|
-
TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
|
35
|
-
ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
|
36
|
-
|
37
|
-
WHITESPACE = " \t\n\r"
|
38
|
-
KEYWORD = %w[true false null].freeze
|
39
|
-
FUNCTIONS = %w[length count match search value].freeze
|
40
|
-
|
41
|
-
# faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
|
42
|
-
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
43
|
-
DIGITS = '0123456789'
|
44
|
-
|
45
|
-
# chars that may be used as the first letter of member-name-shorthand
|
46
|
-
NAME_FIRST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
|
47
|
-
|
48
8
|
# Transforms source code into tokens
|
49
9
|
class Lexer
|
50
10
|
class Error < Janeway::Error; end
|
51
11
|
|
12
|
+
OPERATORS = {
|
13
|
+
and: '&&',
|
14
|
+
array_slice_separator: ':',
|
15
|
+
child_end: ']',
|
16
|
+
child_start: '[',
|
17
|
+
current_node: '@',
|
18
|
+
descendants: '..',
|
19
|
+
dot: '.',
|
20
|
+
equal: '==',
|
21
|
+
filter: '?',
|
22
|
+
greater_than: '>',
|
23
|
+
greater_than_or_equal: '>=',
|
24
|
+
group_end: ')',
|
25
|
+
group_start: '(',
|
26
|
+
less_than: '<',
|
27
|
+
less_than_or_equal: '<=',
|
28
|
+
minus: '-',
|
29
|
+
not: '!',
|
30
|
+
not_equal: '!=',
|
31
|
+
or: '||',
|
32
|
+
root: '$',
|
33
|
+
union: ',',
|
34
|
+
wildcard: '*',
|
35
|
+
}.freeze
|
36
|
+
ONE_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 1 }.freeze
|
37
|
+
TWO_CHAR_LEX = OPERATORS.values.select { |lexeme| lexeme.size == 2 }.freeze
|
38
|
+
TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
|
39
|
+
ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
|
40
|
+
|
41
|
+
WHITESPACE = " \t\n\r"
|
42
|
+
KEYWORD = %w[true false null].freeze
|
43
|
+
FUNCTIONS = %w[length count match search value].freeze
|
44
|
+
|
45
|
+
# faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
|
46
|
+
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
47
|
+
DIGITS = '0123456789'
|
48
|
+
|
49
|
+
# chars that may be used as the first letter of member-name-shorthand
|
50
|
+
NAME_FIRST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
|
51
|
+
|
52
52
|
attr_reader :source, :tokens
|
53
53
|
attr_accessor :next_p, :lexeme_start_p
|
54
54
|
|
data/lib/janeway/parser.rb
CHANGED
data/lib/janeway/query.rb
CHANGED
@@ -30,10 +30,19 @@ module Janeway
|
|
30
30
|
Janeway::Enumerator.new(self, input)
|
31
31
|
end
|
32
32
|
|
33
|
+
# @return [String]
|
33
34
|
def to_s
|
34
35
|
@root.to_s
|
35
36
|
end
|
36
37
|
|
38
|
+
# Return true if this query can only possibly match 0 or 1 elements in any input.
|
39
|
+
# Such a query must be composed exclusively of the root identifier followed by
|
40
|
+
# name selectors and / or index selectors.
|
41
|
+
# @return [Boolean]
|
42
|
+
def singular_query?
|
43
|
+
@root.singular_query?
|
44
|
+
end
|
45
|
+
|
37
46
|
# Return a list of the nodes in the AST.
|
38
47
|
# The AST of a jsonpath query is a straight line, so this is expressible as an array.
|
39
48
|
# The only part of the AST with branches is inside a filter selector, but that doesn't show up here.
|
@@ -66,5 +75,36 @@ module Janeway
|
|
66
75
|
|
67
76
|
result.flatten.join("\n")
|
68
77
|
end
|
78
|
+
|
79
|
+
# Deep copy the query
|
80
|
+
# @return [Query]
|
81
|
+
def dup
|
82
|
+
Parser.parse(to_s)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Delete the last element from the chain of selectors.
|
86
|
+
# For a singular query, this makes the query point to the match's parent instead of the match itself.
|
87
|
+
#
|
88
|
+
# Don't do this for a non-singular query, those may contain child segments and
|
89
|
+
# descendant segments which would lead to different results.
|
90
|
+
#
|
91
|
+
# @return [AST::Selector]
|
92
|
+
def pop
|
93
|
+
unless singular_query?
|
94
|
+
raise Janeway::Error.new('not allowed to pop from a non-singular query', to_s)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sever the link to the last selector
|
98
|
+
nodes = node_list
|
99
|
+
if nodes.size == 1
|
100
|
+
# Special case: cannot pop from the query "$" even though it is a singular query
|
101
|
+
raise Janeway::Error.new('cannot pop from single-element query', to_s)
|
102
|
+
end
|
103
|
+
|
104
|
+
nodes[-2].next = nil
|
105
|
+
|
106
|
+
# Return the last selector
|
107
|
+
nodes.last
|
108
|
+
end
|
69
109
|
end
|
70
110
|
end
|