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.
- checksums.yaml +4 -4
- data/README.md +201 -28
- data/bin/janeway +36 -9
- data/lib/janeway/ast/expression.rb +1 -2
- data/lib/janeway/enumerator.rb +43 -0
- data/lib/janeway/error.rb +6 -9
- data/lib/janeway/functions/length.rb +1 -1
- data/lib/janeway/interpreter.rb +124 -19
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +41 -0
- data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +15 -10
- data/lib/janeway/interpreters/base.rb +26 -2
- data/lib/janeway/interpreters/binary_operator_interpreter.rb +10 -7
- data/lib/janeway/interpreters/child_segment_deleter.rb +19 -0
- data/lib/janeway/interpreters/child_segment_interpreter.rb +48 -8
- data/lib/janeway/interpreters/current_node_interpreter.rb +5 -3
- data/lib/janeway/interpreters/descendant_segment_interpreter.rb +13 -9
- data/lib/janeway/interpreters/filter_selector_deleter.rb +65 -0
- data/lib/janeway/interpreters/filter_selector_interpreter.rb +54 -14
- data/lib/janeway/interpreters/function_interpreter.rb +7 -5
- data/lib/janeway/interpreters/index_selector_deleter.rb +26 -0
- data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -2
- data/lib/janeway/interpreters/name_selector_deleter.rb +27 -0
- data/lib/janeway/interpreters/name_selector_interpreter.rb +19 -4
- data/lib/janeway/interpreters/root_node_deleter.rb +34 -0
- data/lib/janeway/interpreters/root_node_interpreter.rb +4 -2
- data/lib/janeway/interpreters/tree_constructor.rb +20 -1
- data/lib/janeway/interpreters/unary_operator_interpreter.rb +2 -2
- data/lib/janeway/interpreters/wildcard_selector_deleter.rb +32 -0
- data/lib/janeway/interpreters/wildcard_selector_interpreter.rb +26 -11
- data/lib/janeway/interpreters/yielder.rb +50 -12
- data/lib/janeway/lexer.rb +1 -1
- data/lib/janeway/normalized_path.rb +66 -0
- data/lib/janeway/parser.rb +3 -3
- data/lib/janeway/query.rb +70 -0
- data/lib/janeway/version.rb +1 -1
- data/lib/janeway.rb +16 -28
- metadata +12 -3
- data/lib/janeway/ast/query.rb +0 -98
@@ -18,15 +18,17 @@ module Janeway
|
|
18
18
|
value: [:nodes_type],
|
19
19
|
}.freeze
|
20
20
|
|
21
|
-
# @param [AST::Function]
|
21
|
+
# @param function [AST::Function]
|
22
22
|
def initialize(function)
|
23
23
|
super
|
24
24
|
@params = function.parameters.map { |param| TreeConstructor.ast_node_to_interpreter(param) }
|
25
25
|
end
|
26
26
|
|
27
27
|
# @param input [Array, Hash] the results of processing so far
|
28
|
+
# @param _parent [Array, Hash] parent of the input object
|
28
29
|
# @param root [Array, Hash] the entire input
|
29
|
-
|
30
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
31
|
+
def interpret(input, _parent, root, _path)
|
30
32
|
params = interpret_function_parameters(@params, input, root)
|
31
33
|
function.body.call(*params)
|
32
34
|
end
|
@@ -40,8 +42,8 @@ module Janeway
|
|
40
42
|
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-well-typedness-of-function-
|
41
43
|
#
|
42
44
|
# @param parameters [Array] parameters before evaluation
|
43
|
-
# @param func [String] function name (eg. "length", "count")
|
44
45
|
# @param input [Object]
|
46
|
+
# @param root [Array, Hash] the entire input
|
45
47
|
# @return [Array] parameters after evaluation
|
46
48
|
def interpret_function_parameters(parameters, input, root)
|
47
49
|
param_types = FUNCTION_PARAMETER_TYPES[function.name.to_sym]
|
@@ -50,13 +52,13 @@ module Janeway
|
|
50
52
|
type = param_types[i]
|
51
53
|
case parameter.node
|
52
54
|
when AST::CurrentNode, AST::RootNode
|
53
|
-
result = parameter.interpret(input, root)
|
55
|
+
result = parameter.interpret(input, nil, root, [])
|
54
56
|
if type == :value_type && parameter.node.singular_query?
|
55
57
|
deconstruct(result)
|
56
58
|
else
|
57
59
|
result
|
58
60
|
end
|
59
|
-
when AST::Function, AST::StringType then parameter.interpret(input, root)
|
61
|
+
when AST::Function, AST::StringType then parameter.interpret(input, nil, root, [])
|
60
62
|
when String then parameter.value # from a TreeConstructor::Literal
|
61
63
|
else
|
62
64
|
# invalid parameter type. Function must accept it and return Nothing result
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'name_selector_interpreter'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Interprets an index selector, and deletes the matched value.
|
8
|
+
class IndexSelectorDeleter < IndexSelectorInterpreter
|
9
|
+
# Interpret selector on the given input.
|
10
|
+
# @param input [Array, Hash] the results of processing so far
|
11
|
+
# @param _parent [Array, Hash] parent of the input object
|
12
|
+
# @param _root [Array, Hash] the entire input
|
13
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
14
|
+
def interpret(input, _parent, _root, _path)
|
15
|
+
return [] unless input.is_a?(Array)
|
16
|
+
|
17
|
+
index = selector.value
|
18
|
+
result = input.fetch(index) # raises IndexError if no such index
|
19
|
+
input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
|
20
|
+
[result]
|
21
|
+
rescue IndexError
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -24,13 +24,14 @@ module Janeway
|
|
24
24
|
#
|
25
25
|
# @param input [Array, Hash] the results of processing so far
|
26
26
|
# @param root [Array, Hash] the entire input
|
27
|
-
|
27
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
28
|
+
def interpret(input, _parent, root, path)
|
28
29
|
return [] unless input.is_a?(Array)
|
29
30
|
|
30
31
|
result = input.fetch(selector.value) # raises IndexError if no such index
|
31
32
|
return [result] unless @next
|
32
33
|
|
33
|
-
@next.interpret(result, root)
|
34
|
+
@next.interpret(result, input, root, path + [selector.value])
|
34
35
|
rescue IndexError
|
35
36
|
[]
|
36
37
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'name_selector_interpreter'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Interprets a name selector, and deletes the matched values.
|
8
|
+
class NameSelectorDeleter < NameSelectorInterpreter
|
9
|
+
# Interpret selector on the given input.
|
10
|
+
# @param input [Array, Hash] the results of processing so far
|
11
|
+
# @param _parent [Array, Hash] parent of the input object
|
12
|
+
# @param _root [Array, Hash] the entire input
|
13
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
14
|
+
def interpret(input, _parent, _root, _path)
|
15
|
+
return [] unless input.is_a?(Hash) && input.key?(name)
|
16
|
+
|
17
|
+
[input.delete(name)]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return hash representation of this interpreter
|
21
|
+
# @return [Hash]
|
22
|
+
def as_json
|
23
|
+
{ type: type, value: name, next: @next&.as_json }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -12,18 +12,33 @@ module Janeway
|
|
12
12
|
# and a key that does not exist ([])
|
13
13
|
class NameSelectorInterpreter < Base
|
14
14
|
alias selector node
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# @param selector [AST::NameSelector]
|
18
|
+
def initialize(selector)
|
19
|
+
super
|
20
|
+
@name = selector.name
|
21
|
+
end
|
15
22
|
|
16
23
|
# Interpret selector on the given input.
|
17
24
|
# @param input [Array, Hash] the results of processing so far
|
25
|
+
# @param _parent [Array, Hash] parent of the input object
|
18
26
|
# @param root [Array, Hash] the entire input
|
19
|
-
|
20
|
-
|
27
|
+
# @param path [Array<String>] 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)
|
21
30
|
|
22
|
-
result = input[
|
31
|
+
result = input[name]
|
23
32
|
return [result] unless @next
|
24
33
|
|
25
34
|
# Forward result to next selector
|
26
|
-
@next.interpret(result, root)
|
35
|
+
@next.interpret(result, input, root, path + [name])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return hash representation of this interpreter
|
39
|
+
# @return [Hash]
|
40
|
+
def as_json
|
41
|
+
{ type: type, value: name, next: @next&.as_json }
|
27
42
|
end
|
28
43
|
end
|
29
44
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'wildcard_selector_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
|
@@ -12,11 +12,13 @@ module Janeway
|
|
12
12
|
# Start an expression chain using the entire, unfiltered input.
|
13
13
|
#
|
14
14
|
# @param _input [Array, Hash] the results of processing so far
|
15
|
+
# @param _parent [Array, Hash] parent of the input object
|
15
16
|
# @param root [Array, Hash] the entire input
|
16
|
-
|
17
|
+
# @param _path [Array<String>] elements of normalized path to the current input
|
18
|
+
def interpret(_input, _parent, root, _path = nil)
|
17
19
|
return [root] unless @next
|
18
20
|
|
19
|
-
@next.interpret(root, root)
|
21
|
+
@next.interpret(root, nil, root, ['$'])
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
@@ -4,7 +4,10 @@ require_relative 'base'
|
|
4
4
|
|
5
5
|
module Janeway
|
6
6
|
module Interpreters
|
7
|
-
# Constructs a tree of interpreter objects
|
7
|
+
# Constructs a tree of interpreter objects.
|
8
|
+
#
|
9
|
+
# It is used when an iterator method such as #each has been called to
|
10
|
+
# construct the chain of interpreter objects to handle the query.
|
8
11
|
module TreeConstructor
|
9
12
|
# Fake interpreter which just returns the given value
|
10
13
|
Literal = Struct.new(:value) do
|
@@ -34,6 +37,22 @@ module Janeway
|
|
34
37
|
raise "Unknown AST expression: #{expr.inspect}"
|
35
38
|
end
|
36
39
|
end
|
40
|
+
|
41
|
+
def self.ast_node_to_deleter(expr)
|
42
|
+
case expr
|
43
|
+
when AST::IndexSelector then IndexSelectorDeleter.new(expr)
|
44
|
+
when AST::ArraySliceSelector then ArraySliceSelectorDeleter.new(expr)
|
45
|
+
when AST::NameSelector then NameSelectorDeleter.new(expr)
|
46
|
+
when AST::FilterSelector then FilterSelectorDeleter.new(expr)
|
47
|
+
when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
|
48
|
+
when AST::ChildSegment then ChildSegmentDeleter.new(expr)
|
49
|
+
when AST::RootNode then RootNodeDeleter.new(expr)
|
50
|
+
|
51
|
+
when nil then nil # caller has no @next node
|
52
|
+
else
|
53
|
+
raise "Unknown AST expression: #{expr.inspect}"
|
54
|
+
end
|
55
|
+
end
|
37
56
|
end
|
38
57
|
end
|
39
58
|
end
|
@@ -18,8 +18,8 @@ module Janeway
|
|
18
18
|
end
|
19
19
|
|
20
20
|
# @return [Boolean]
|
21
|
-
def interpret(input, root)
|
22
|
-
result = @operand.interpret(input, root)
|
21
|
+
def interpret(input, parent, root, _path)
|
22
|
+
result = @operand.interpret(input, parent, root, [])
|
23
23
|
case result
|
24
24
|
when Array then result.empty?
|
25
25
|
when TrueClass, FalseClass then !result
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'wildcard_selector_interpreter'
|
4
|
+
|
5
|
+
module Janeway
|
6
|
+
module Interpreters
|
7
|
+
# Interprets a wildcard selector, and deletes the results.
|
8
|
+
class WildcardSelectorDeleter < WildcardSelectorInterpreter
|
9
|
+
# Delete all elements from the input
|
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] deleted elements
|
16
|
+
def interpret(input, _parent, _root, _path)
|
17
|
+
case input
|
18
|
+
when Array
|
19
|
+
results = input.dup
|
20
|
+
input.clear
|
21
|
+
results
|
22
|
+
when Hash
|
23
|
+
results = input.values
|
24
|
+
input.clear
|
25
|
+
results
|
26
|
+
else
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -14,22 +14,37 @@ module Janeway
|
|
14
14
|
# For anything else, return empty list.
|
15
15
|
#
|
16
16
|
# @param input [Array, Hash] the results of processing so far
|
17
|
+
# @param _parent [Array, Hash] parent of the input object
|
17
18
|
# @param root [Array, Hash] the entire input
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
# @param path [Array<String>] elements of normalized path to the current input
|
20
|
+
def interpret(input, _parent, root, path)
|
21
|
+
case input
|
22
|
+
when Array then interpret_array(input, root, path)
|
23
|
+
when Hash then interpret_hash(input, root, path)
|
24
|
+
else []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def interpret_hash(input, root, path)
|
29
|
+
return [] if input.empty? # early exit, no need for further processing on empty list
|
30
|
+
return input.values unless @next
|
31
|
+
|
32
|
+
# Apply child selector to each node in the output node list
|
33
|
+
results = []
|
34
|
+
input.each do |key, value|
|
35
|
+
results.concat @next.interpret(value, input, root, path + [key])
|
36
|
+
end
|
37
|
+
results
|
38
|
+
end
|
25
39
|
|
26
|
-
|
27
|
-
return
|
40
|
+
def interpret_array(input, root, path)
|
41
|
+
return input if input.empty? # early exit, no need for further processing on empty list
|
42
|
+
return input unless @next
|
28
43
|
|
29
44
|
# Apply child selector to each node in the output node list
|
30
45
|
results = []
|
31
|
-
|
32
|
-
results.concat @next.interpret(value, root)
|
46
|
+
input.each_with_index do |value, i|
|
47
|
+
results.concat @next.interpret(value, input, root, path + [i])
|
33
48
|
end
|
34
49
|
results
|
35
50
|
end
|
@@ -1,34 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'base'
|
4
|
+
require_relative '../normalized_path'
|
4
5
|
|
5
6
|
module Janeway
|
6
7
|
module Interpreters
|
7
8
|
# Yields each input value.
|
8
|
-
# It is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
|
9
|
-
# This is a supporting class for the supports the Janeway.each method.
|
10
9
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
14
|
-
class Yielder < Base
|
15
|
-
# The implicit constructor forwards a closure to the base class constructor.
|
16
|
-
# Base class constructor stores it in @node.
|
10
|
+
# It is inserted at the end of the "real" selectors in the AST, to receive and yield the output.
|
11
|
+
# This is a supporting class for the Janeway.each method.
|
12
|
+
class Yielder
|
17
13
|
def initialize(&block)
|
18
|
-
super(Struct.new(:next).new)
|
19
14
|
@block = block
|
15
|
+
|
16
|
+
# Decide how many parameters to yield to this block.
|
17
|
+
# block.arity is -1 when no block was given, and an enumerator is being returned from #each
|
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
|
20
29
|
end
|
21
30
|
|
22
31
|
# Yield each input value
|
23
32
|
#
|
24
33
|
# @param input [Array, Hash] the results of processing so far
|
34
|
+
# @param parent [Array, Hash] parent of the input object
|
25
35
|
# @param _root [Array, Hash] the entire input
|
36
|
+
# @param path [Array<String, Integer>] components of normalized path to the current input
|
26
37
|
# @yieldparam [Object] matched value
|
27
|
-
# @return [Object] input
|
28
|
-
def interpret(input, _root)
|
29
|
-
@
|
38
|
+
# @return [Object] input as node list
|
39
|
+
def interpret(input, parent, _root, path)
|
40
|
+
@yield_to_block.call(input, parent, path)
|
30
41
|
input.is_a?(Array) ? input : [input]
|
31
42
|
end
|
43
|
+
|
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
|
+
# Dummy method from Interpreters::Base, allow child segment interpreter to disable the
|
62
|
+
# non-exist 'next' link.
|
63
|
+
# @return [void]
|
64
|
+
def next=(*); end
|
65
|
+
|
66
|
+
# @return [Hash]
|
67
|
+
def as_json
|
68
|
+
{ type: self.class.to_s.split('::').last }
|
69
|
+
end
|
32
70
|
end
|
33
71
|
end
|
34
72
|
end
|
data/lib/janeway/lexer.rb
CHANGED
@@ -170,7 +170,7 @@ module Janeway
|
|
170
170
|
def lex_delimited_string(delimiter)
|
171
171
|
allowed_delimiters = %w[' "]
|
172
172
|
# the "other" delimiter char, which is not currently being treated as a delimiter
|
173
|
-
non_delimiter = allowed_delimiters.reject {
|
173
|
+
non_delimiter = allowed_delimiters.reject { |char| char == delimiter }.first
|
174
174
|
|
175
175
|
literal_chars = []
|
176
176
|
while lookahead != delimiter && source_uncompleted?
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janeway
|
4
|
+
# Converts index and name selector values to normalized path components.
|
5
|
+
#
|
6
|
+
# This implements the normalized path description in RFC 9535:
|
7
|
+
# @see https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths
|
8
|
+
#
|
9
|
+
# This does a lot of escaping, and much of it is the inverse of Lexer code that does un-escaping.
|
10
|
+
module NormalizedPath
|
11
|
+
# Characters that do not need escaping, defined by hexadecimal range
|
12
|
+
NORMAL_UNESCAPED_RANGES = [(0x20..0x26), (0x28..0x5B), (0x5D..0xD7FF), (0xE000..0x10FFFF)].freeze
|
13
|
+
|
14
|
+
def self.normalize(value)
|
15
|
+
case value
|
16
|
+
when String then normalize_name(value)
|
17
|
+
when Integer then normalize_index(value)
|
18
|
+
else
|
19
|
+
raise "Cannot normalize #{value.inspect}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param index [Integer] index selector value
|
24
|
+
# @return [String] eg. "[1]"
|
25
|
+
def self.normalize_index(index)
|
26
|
+
"[#{index}]"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param name [Integer] name selector value
|
30
|
+
# @return [String] eg. "['']"
|
31
|
+
def self.normalize_name(name)
|
32
|
+
"['#{escape(name)}']"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.escape(str)
|
36
|
+
# Common case, all chars are normal.
|
37
|
+
return str if str.chars.all? { |char| NORMAL_UNESCAPED_RANGES.any? { |range| range.include?(char.ord) } }
|
38
|
+
|
39
|
+
# Some escaping must be done
|
40
|
+
str.chars.map { |char| escape_char(char) }.join
|
41
|
+
end
|
42
|
+
|
43
|
+
# Escape or hex-encode the given character
|
44
|
+
# @param char [String] single character, possibly multi-byte
|
45
|
+
# @return [String]
|
46
|
+
def self.escape_char(char)
|
47
|
+
# Character ranges defined by https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7-8
|
48
|
+
case char.ord
|
49
|
+
when 0x20..0x26, 0x28..0x5B, 0x5D..0xD7FF, 0xE000..0x10FFFF # normal-unescaped range
|
50
|
+
char # unescaped
|
51
|
+
when 0x62, 0x66, 0x6E, 0x72, 0x74, 0x27, 0x5C # normal-escapable range
|
52
|
+
# backspace, form feed, line feed, carriage return, horizontal tab, apostrophe, backslash
|
53
|
+
"\\#{char}" # escaped
|
54
|
+
else # normal-hexchar range
|
55
|
+
hex_encode_char(char)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Hex-encode the given character
|
60
|
+
# @param char [String] single character, possibly multi-byte
|
61
|
+
# @return [String]
|
62
|
+
def self.hex_encode_char(char)
|
63
|
+
format('\\u00%02x', char.ord)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/janeway/parser.rb
CHANGED
@@ -29,7 +29,7 @@ module Janeway
|
|
29
29
|
}.freeze
|
30
30
|
|
31
31
|
# @param jsonpath [String] jsonpath query to be lexed and parsed
|
32
|
-
# @return [
|
32
|
+
# @return [Query]
|
33
33
|
def self.parse(jsonpath)
|
34
34
|
raise ArgumentError, "expect jsonpath string, got #{jsonpath.inspect}" unless jsonpath.is_a?(String)
|
35
35
|
|
@@ -46,7 +46,7 @@ module Janeway
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# Parse the token list and create an Abstract Syntax Tree
|
49
|
-
# @return [
|
49
|
+
# @return [Query]
|
50
50
|
def parse
|
51
51
|
consume
|
52
52
|
raise err('JsonPath queries must start with root identifier "$"') unless current.type == :root
|
@@ -58,7 +58,7 @@ module Janeway
|
|
58
58
|
raise err("Unrecognized expressions after query: #{remaining}")
|
59
59
|
end
|
60
60
|
|
61
|
-
|
61
|
+
Query.new(root_node, @jsonpath)
|
62
62
|
end
|
63
63
|
|
64
64
|
private
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janeway
|
4
|
+
# Query holds the abstract syntax tree created by parsing the query.
|
5
|
+
# This can be frozen and passed to multiple threads or ractors for simultaneous use.
|
6
|
+
# No instance members are modified during the interpretation stage.
|
7
|
+
class Query
|
8
|
+
# The original jsonpath query, for use in error messages
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :jsonpath
|
11
|
+
|
12
|
+
# @return [AST::Root]
|
13
|
+
attr_reader :root
|
14
|
+
|
15
|
+
# @param root_node [AST::Root]
|
16
|
+
# @param jsonpath [String]
|
17
|
+
def initialize(root_node, jsonpath)
|
18
|
+
raise ArgumentError, "expect root identifier, got #{root_node.inspect}" unless root_node.is_a?(AST::RootNode)
|
19
|
+
raise ArgumentError, "expect query string, got #{jsonpath.inspect}" unless jsonpath.is_a?(String)
|
20
|
+
|
21
|
+
@root = root_node
|
22
|
+
@jsonpath = jsonpath
|
23
|
+
end
|
24
|
+
|
25
|
+
# Combine this query with input to make an Enumerator.
|
26
|
+
# This can be used to iterate over results with #each, #map, etc.
|
27
|
+
#
|
28
|
+
# @return [Janeway::Enumerator]
|
29
|
+
def on(input)
|
30
|
+
Janeway::Enumerator.new(self, input)
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
@root.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return a list of the nodes in the AST.
|
38
|
+
# The AST of a jsonpath query is a straight line, so this is expressible as an array.
|
39
|
+
# The only part of the AST with branches is inside a filter selector, but that doesn't show up here.
|
40
|
+
# @return [Array<Expression>]
|
41
|
+
def node_list
|
42
|
+
nodes = []
|
43
|
+
node = @root
|
44
|
+
loop do
|
45
|
+
nodes << node
|
46
|
+
break unless node.next
|
47
|
+
|
48
|
+
node = node.next
|
49
|
+
end
|
50
|
+
nodes
|
51
|
+
end
|
52
|
+
|
53
|
+
# Queries are considered equal if their ASTs evaluate to the same JSONPath string.
|
54
|
+
#
|
55
|
+
# The string output is generated from the AST and should be considered a "normalized"
|
56
|
+
# form of the query. It may have different whitespace and parentheses than the original
|
57
|
+
# input but will be semantically equivalent.
|
58
|
+
def ==(other)
|
59
|
+
to_s == other.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Print AST in tree format
|
63
|
+
# Every AST class prints a 1-line representation of self, with children on separate lines
|
64
|
+
def tree
|
65
|
+
result = @root.tree(0)
|
66
|
+
|
67
|
+
result.flatten.join("\n")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/janeway/version.rb
CHANGED
data/lib/janeway.rb
CHANGED
@@ -11,45 +11,33 @@ module Janeway
|
|
11
11
|
INTEGER_MAX = 9_007_199_254_740_991
|
12
12
|
end
|
13
13
|
|
14
|
-
#
|
14
|
+
# Pair a jsonpath query with data to make an enumerator.
|
15
|
+
# This can be used to apply the query to the data using Enumerator module
|
16
|
+
# methods such as #each and #map.
|
15
17
|
#
|
16
|
-
# @param
|
17
|
-
# @param
|
18
|
-
# @return [
|
19
|
-
def self.
|
20
|
-
|
21
|
-
Janeway::
|
18
|
+
# @param jsonpath [String] jsonpath query
|
19
|
+
# @param data [Array, Hash] input data
|
20
|
+
# @return [Janeway::Enumerator]
|
21
|
+
def self.enum_for(jsonpath, data)
|
22
|
+
query = compile(jsonpath)
|
23
|
+
Janeway::Enumerator.new(query, data)
|
22
24
|
end
|
23
25
|
|
24
26
|
# Compile a JSONPath query into an Abstract Syntax Tree.
|
25
27
|
#
|
26
|
-
# This can be
|
28
|
+
# This can be combined with inputs (using #on) to create Enumerators.
|
29
|
+
# @example
|
30
|
+
# query = Janeway.compile('$.store.books[? length(@.title) > 20]')
|
31
|
+
# long_title_books = query.on(local_json).search
|
32
|
+
# query.on(remote_json).each do |book|
|
33
|
+
# long_title_books << book
|
34
|
+
# end
|
27
35
|
#
|
28
36
|
# @param query [String] jsonpath query
|
29
37
|
# @return [Janeway::AST::Query]
|
30
38
|
def self.compile(query)
|
31
39
|
Janeway::Parser.parse(query)
|
32
40
|
end
|
33
|
-
|
34
|
-
# Iterate through each value matched by the JSONPath query.
|
35
|
-
#
|
36
|
-
# @param query [String] jsonpath query
|
37
|
-
# @param input [Hash, Array] ruby object to be searched
|
38
|
-
# @yieldparam [Object] matched value
|
39
|
-
# @return [void]
|
40
|
-
def self.each(query, input, &block)
|
41
|
-
raise ArgumentError, "Invalid jsonpath query: #{query.inspect}" unless query.is_a?(String)
|
42
|
-
unless [Hash, Array, String].include?(input.class)
|
43
|
-
raise ArgumentError, "Invalid input, expecting array or hash: #{input.inspect}"
|
44
|
-
end
|
45
|
-
return enum_for(:each, query, input) unless block_given?
|
46
|
-
|
47
|
-
ast = Janeway::Parser.parse(query)
|
48
|
-
interpreter = Janeway::Interpreter.new(ast)
|
49
|
-
yielder = Janeway::Interpreters::Yielder.new(&block)
|
50
|
-
interpreter.push(yielder)
|
51
|
-
interpreter.interpret(input)
|
52
|
-
end
|
53
41
|
end
|
54
42
|
|
55
43
|
# Require ruby source files in the given dir. Do not recurse to subdirs.
|