jmespath-ruby 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +57 -0
- data/LICENSE +21 -0
- data/LICENSES/python-MIT.txt +21 -0
- data/LICENSES/ruby-Apache-2.0.txt +174 -0
- data/README.md +122 -0
- data/SECURITY.md +16 -0
- data/VERSIONING.md +12 -0
- data/docs/guides/compatibility.md +84 -0
- data/docs/guides/compilation.md +63 -0
- data/docs/guides/errors.md +30 -0
- data/docs/guides/functions.md +65 -0
- data/docs/guides/performance.md +237 -0
- data/docs/guides/search.md +56 -0
- data/exe/jmespath +5 -0
- data/exe/jmespath.rb +5 -0
- data/lib/jmespath/ast.rb +43 -0
- data/lib/jmespath/cache.rb +53 -0
- data/lib/jmespath/cli.rb +36 -0
- data/lib/jmespath/data_access.rb +66 -0
- data/lib/jmespath/errors.rb +53 -0
- data/lib/jmespath/evaluation/construction.rb +20 -0
- data/lib/jmespath/evaluation/logic.rb +42 -0
- data/lib/jmespath/evaluation/projections.rb +43 -0
- data/lib/jmespath/evaluation/scalars.rb +49 -0
- data/lib/jmespath/evaluation/slices.rb +50 -0
- data/lib/jmespath/evaluator.rb +37 -0
- data/lib/jmespath/expression.rb +63 -0
- data/lib/jmespath/expression_reference.rb +15 -0
- data/lib/jmespath/functions/coercions.rb +41 -0
- data/lib/jmespath/functions/collections.rb +84 -0
- data/lib/jmespath/functions/conversion.rb +32 -0
- data/lib/jmespath/functions/function_call.rb +32 -0
- data/lib/jmespath/functions/references.rb +62 -0
- data/lib/jmespath/functions/scalar.rb +74 -0
- data/lib/jmespath/functions/signature.rb +77 -0
- data/lib/jmespath/functions.rb +73 -0
- data/lib/jmespath/input.rb +19 -0
- data/lib/jmespath/lexer.rb +132 -0
- data/lib/jmespath/normalizer.rb +53 -0
- data/lib/jmespath/options.rb +67 -0
- data/lib/jmespath/parser/expressions.rb +70 -0
- data/lib/jmespath/parser/projections.rb +91 -0
- data/lib/jmespath/parser.rb +119 -0
- data/lib/jmespath/plan.rb +153 -0
- data/lib/jmespath/token.rb +6 -0
- data/lib/jmespath/types.rb +84 -0
- data/lib/jmespath/version.rb +7 -0
- data/lib/jmespath.rb +100 -0
- data/sig/jmespath.rbs +55 -0
- metadata +135 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
class Signature
|
|
7
|
+
TYPES = %i[any null boolean number string array object expref array_number array_string].freeze
|
|
8
|
+
|
|
9
|
+
# Identifies the invocation that created a validation failure.
|
|
10
|
+
module FailureOrigin
|
|
11
|
+
attr_reader :origin
|
|
12
|
+
|
|
13
|
+
def initialize(message = nil, origin: nil)
|
|
14
|
+
@origin = origin
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# An error raised by the library's argument validator.
|
|
20
|
+
class TypeMismatch < Errors::InvalidTypeError
|
|
21
|
+
include FailureOrigin
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# An error raised by the library's arity validator.
|
|
25
|
+
class ArityMismatch < Errors::InvalidArityError
|
|
26
|
+
include FailureOrigin
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(arguments, variadic: false)
|
|
30
|
+
raise ArgumentError, "Invalid function signature" unless valid_definition?(arguments)
|
|
31
|
+
raise ArgumentError, "Variadic signatures require an argument" if variadic && arguments.empty?
|
|
32
|
+
|
|
33
|
+
@arguments = arguments.map { |types| types.dup.freeze }.freeze
|
|
34
|
+
@variadic = variadic
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate!(values, name, origin: nil)
|
|
39
|
+
unless valid_arity?(values.length)
|
|
40
|
+
raise ArityMismatch.new("Invalid number of arguments for #{name}", origin: origin)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
values.each_with_index do |value, index|
|
|
44
|
+
types = @arguments.fetch(index) { @arguments.fetch(-1) }
|
|
45
|
+
next if types.any? { |type| matches?(value, type) }
|
|
46
|
+
|
|
47
|
+
raise TypeMismatch.new("Invalid argument #{index + 1} for #{name}: #{Types.name(value)}", origin: origin)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def valid_definition?(arguments)
|
|
54
|
+
arguments.is_a?(Array) && arguments.all? do |types|
|
|
55
|
+
types.is_a?(Array) && !types.empty? && (types - TYPES).empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def valid_arity?(count)
|
|
60
|
+
@variadic ? count >= @arguments.length : count == @arguments.length
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def matches?(value, type)
|
|
64
|
+
return true if type == :any
|
|
65
|
+
return array_matches?(value, :number) if type == :array_number
|
|
66
|
+
return array_matches?(value, :string) if type == :array_string
|
|
67
|
+
|
|
68
|
+
Types.name(value) == type
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def array_matches?(value, type)
|
|
72
|
+
array = DataAccess.array(value)
|
|
73
|
+
array&.all? { |item| Types.name(item) == type }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# Immutable definitions for functions available to an expression.
|
|
5
|
+
class Functions
|
|
6
|
+
# @api private
|
|
7
|
+
Definition = Data.define(:signature, :implementation)
|
|
8
|
+
|
|
9
|
+
# Create an empty immutable function registry.
|
|
10
|
+
def initialize
|
|
11
|
+
# @type var definitions: Hash[String, Definition]
|
|
12
|
+
definitions = {}
|
|
13
|
+
@definitions = definitions.freeze
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Return an independent registry with one additional definition.
|
|
18
|
+
# @param name [String] unquoted function identifier
|
|
19
|
+
# @param signature [Array<Array<Symbol>>] accepted types for each argument
|
|
20
|
+
# @param variadic [Boolean] repeat the final argument type
|
|
21
|
+
# @param override [Boolean] explicitly replace a definition
|
|
22
|
+
# @yieldparam arguments [Object] evaluated arguments
|
|
23
|
+
# @yieldreturn [Object] the function result
|
|
24
|
+
# @raise [ArgumentError] the definition is invalid or duplicates an existing name
|
|
25
|
+
# @return [Functions] the extended registry
|
|
26
|
+
def with(name, signature:, variadic: false, override: false, &block)
|
|
27
|
+
raise ArgumentError, "A function block is required" unless block
|
|
28
|
+
|
|
29
|
+
definition = Definition.new(Signature.new(signature, variadic: variadic), block)
|
|
30
|
+
add(name, definition, override: override)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @api private
|
|
34
|
+
def with_builtin(name, signature:, handler:, variadic: false)
|
|
35
|
+
definition = Definition.new(Signature.new(signature, variadic: variadic), handler)
|
|
36
|
+
add(name, definition, override: false)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Evaluate a registered function with checked arguments.
|
|
40
|
+
# @param name [String] function identifier
|
|
41
|
+
# @param arguments [Array<Object>] evaluated values
|
|
42
|
+
# @param disable_visit_errors [Boolean] return nil for validation failures
|
|
43
|
+
# @return [Object] the callback result
|
|
44
|
+
# @raise [Errors::UnknownFunctionError] the name is absent
|
|
45
|
+
# @raise [Errors::InvalidTypeError] argument types are incompatible
|
|
46
|
+
# @raise [Errors::InvalidArityError] the number of arguments is invalid
|
|
47
|
+
def call(name, arguments, disable_visit_errors: false)
|
|
48
|
+
definition = @definitions.fetch(name) { raise Errors::UnknownFunctionError, "Unknown function: #{name}" }
|
|
49
|
+
invocation = disable_visit_errors ? FunctionCall.new : FunctionCall::STRICT
|
|
50
|
+
invocation.call(definition, name, arguments, disable_visit_errors: disable_visit_errors)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api private
|
|
54
|
+
def definition(name)
|
|
55
|
+
@definitions[name]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def add(name, definition, override:)
|
|
61
|
+
unless name.is_a?(String) && name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
62
|
+
raise ArgumentError, "Invalid function name"
|
|
63
|
+
end
|
|
64
|
+
raise ArgumentError, "Function #{name} is already registered" if @definitions.key?(name) && !override
|
|
65
|
+
|
|
66
|
+
result = dup
|
|
67
|
+
result.instance_variable_set(:@definitions, @definitions.merge(name.dup.freeze => definition).freeze)
|
|
68
|
+
result.freeze
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
require_relative "functions/signature"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module JMESPath
|
|
7
|
+
# @api private
|
|
8
|
+
module Input
|
|
9
|
+
def self.load(data)
|
|
10
|
+
return data if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Struct)
|
|
11
|
+
|
|
12
|
+
case data
|
|
13
|
+
when Pathname then File.open(data, "r", encoding: "UTF-8") { |file| JSON.parse(file.read) }
|
|
14
|
+
when IO, StringIO then JSON.parse(data.read.to_s)
|
|
15
|
+
else data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module JMESPath
|
|
6
|
+
# @api private
|
|
7
|
+
class Lexer
|
|
8
|
+
SINGLE = {
|
|
9
|
+
"[" => :lbracket, "]" => :rbracket, "{" => :lbrace, "}" => :rbrace,
|
|
10
|
+
"(" => :lparen, ")" => :rparen, "," => :comma, ":" => :colon,
|
|
11
|
+
"." => :dot, "*" => :star, "@" => :current, "&" => :expref,
|
|
12
|
+
"|" => :pipe, "!" => :not, "<" => :lt, ">" => :gt
|
|
13
|
+
}.freeze
|
|
14
|
+
DOUBLE = {
|
|
15
|
+
"[]" => :flatten, "[?" => :filter, "&&" => :and, "||" => :or,
|
|
16
|
+
"==" => :eq, "!=" => :ne, "<=" => :lte, ">=" => :gte
|
|
17
|
+
}.freeze
|
|
18
|
+
WHITESPACE = [9, 10, 13, 32].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :source
|
|
21
|
+
|
|
22
|
+
def initialize(source, max_depth: 128)
|
|
23
|
+
@source = source.encode(Encoding::UTF_8).freeze
|
|
24
|
+
raise Encoding::InvalidByteSequenceError unless @source.valid_encoding?
|
|
25
|
+
|
|
26
|
+
@index = 0
|
|
27
|
+
@max_depth = max_depth
|
|
28
|
+
rescue EncodingError
|
|
29
|
+
raise Errors::SyntaxError, "Expression has invalid encoding"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tokens
|
|
33
|
+
# @type var result: Array[Token?]
|
|
34
|
+
result = []
|
|
35
|
+
result << next_token while @index < source.bytesize
|
|
36
|
+
result << Token.new(:eof, nil, @index, @index)
|
|
37
|
+
result.compact.freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def next_token
|
|
43
|
+
start = @index
|
|
44
|
+
byte = source.getbyte(@index)
|
|
45
|
+
return skip_whitespace if WHITESPACE.include?(byte)
|
|
46
|
+
return quoted_token(start, byte) if [34, 39, 96].include?(byte)
|
|
47
|
+
return word_token(start) if identifier_start?(byte)
|
|
48
|
+
return number_token(start) if byte == 45 || digit?(byte)
|
|
49
|
+
|
|
50
|
+
operator_token(start)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def skip_whitespace
|
|
54
|
+
@index += 1 while WHITESPACE.include?(source.getbyte(@index))
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def word_token(start)
|
|
59
|
+
@index += 1 while identifier_part?(source.getbyte(@index))
|
|
60
|
+
Token.new(:identifier, source.byteslice(start...@index), start, @index)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def number_token(start)
|
|
64
|
+
@index += 1 if source.getbyte(@index) == 45
|
|
65
|
+
syntax!("Expected a digit", start) unless digit?(source.getbyte(@index))
|
|
66
|
+
@index += 1 while digit?(source.getbyte(@index))
|
|
67
|
+
Token.new(:number, source.byteslice(start...@index).to_i, start, @index)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def operator_token(start)
|
|
71
|
+
pair = source.byteslice(start, 2)
|
|
72
|
+
type = DOUBLE[pair]
|
|
73
|
+
length = type ? 2 : 1
|
|
74
|
+
type ||= SINGLE[source.byteslice(start, 1)]
|
|
75
|
+
syntax!("Unexpected character", start) unless type
|
|
76
|
+
@index += length
|
|
77
|
+
Token.new(type, nil, start, @index)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def quoted_token(start, quote)
|
|
81
|
+
@index += 1
|
|
82
|
+
content_start = @index
|
|
83
|
+
scan_quote(quote, start)
|
|
84
|
+
content = source.byteslice(content_start...@index).to_s
|
|
85
|
+
@index += 1
|
|
86
|
+
value = decode(content, quote)
|
|
87
|
+
Token.new(quote == 34 ? :identifier : :literal, value, start, @index)
|
|
88
|
+
rescue JSON::ParserError => e
|
|
89
|
+
syntax!("Invalid quoted value: #{e.message}", start)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def scan_quote(quote, start)
|
|
93
|
+
while @index < source.bytesize
|
|
94
|
+
return if source.getbyte(@index) == quote
|
|
95
|
+
|
|
96
|
+
@index += source.getbyte(@index) == 92 ? 2 : 1
|
|
97
|
+
end
|
|
98
|
+
syntax!("Unterminated quoted value", start)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def decode(content, quote)
|
|
102
|
+
return JSON.parse("\"#{content}\"") if quote == 34
|
|
103
|
+
return content.gsub("\\'", "'") if quote == 39
|
|
104
|
+
|
|
105
|
+
literal(content.gsub("\\#{96.chr}", 96.chr))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def literal(content)
|
|
109
|
+
JSON.parse(content, max_nesting: @max_depth)
|
|
110
|
+
rescue JSON::NestingError
|
|
111
|
+
raise
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
JSON.parse("\"#{content.lstrip}\"", max_nesting: @max_depth)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def identifier_start?(byte)
|
|
117
|
+
byte && (byte.between?(65, 90) || byte.between?(97, 122) || byte == 95)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def identifier_part?(byte)
|
|
121
|
+
identifier_start?(byte) || digit?(byte)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def digit?(byte)
|
|
125
|
+
byte&.between?(48, 57)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def syntax!(message, position)
|
|
129
|
+
raise Errors::SyntaxError.new(message, expression: source, position: position)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Normalizer
|
|
6
|
+
def initialize
|
|
7
|
+
@pending = []
|
|
8
|
+
@active = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(value)
|
|
12
|
+
# @type var result: Array[value]
|
|
13
|
+
result = [nil]
|
|
14
|
+
enqueue(value) { |entry| result[0] = entry }
|
|
15
|
+
while (operation = @pending.pop)
|
|
16
|
+
operation.call
|
|
17
|
+
end
|
|
18
|
+
result.first
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def enqueue(value, &assign)
|
|
24
|
+
@pending << -> { visit(value, &assign) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def visit(value, &assign)
|
|
28
|
+
collection = DataAccess.array(value) || DataAccess.object(value)
|
|
29
|
+
return assign.call(DataAccess.string(value) || value) unless collection
|
|
30
|
+
|
|
31
|
+
id = value.object_id
|
|
32
|
+
raise Errors::InvalidValueError, "Cyclic values cannot be compared or serialized" if @active[id]
|
|
33
|
+
|
|
34
|
+
@active[id] = true
|
|
35
|
+
@pending << -> { @active.delete(id) }
|
|
36
|
+
collection.is_a?(Array) ? array(collection, &assign) : object(collection, &assign)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def array(values)
|
|
40
|
+
# @type var result: Array[value]
|
|
41
|
+
result = Array.new(values.length)
|
|
42
|
+
yield result
|
|
43
|
+
(values.length - 1).downto(0) { |index| enqueue(values[index]) { |entry| result[index] = entry } }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def object(values)
|
|
47
|
+
# @type var result: Hash[value, value]
|
|
48
|
+
result = values.transform_values { nil }
|
|
49
|
+
yield result
|
|
50
|
+
values.to_a.reverse_each { |key, value| enqueue(value) { |entry| result[key] = entry } }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Options
|
|
6
|
+
# rubocop:disable-next Style/RedundantParentheses -- Type the empty hash before freezing it.
|
|
7
|
+
EMPTY = (
|
|
8
|
+
{} # : Hash[Symbol, value]
|
|
9
|
+
).freeze
|
|
10
|
+
KEYS = %i[cache_expressions disable_visit_errors functions max_depth].freeze
|
|
11
|
+
attr_reader :cache_expressions, :disable_visit_errors, :functions, :max_depth
|
|
12
|
+
|
|
13
|
+
def initialize(positional = {}, keywords = {}, default_depth: 128, defaults: nil)
|
|
14
|
+
values = merge(positional, keywords)
|
|
15
|
+
@cache_expressions = boolean(values.fetch(:cache_expressions, true))
|
|
16
|
+
@disable_visit_errors = boolean(values.fetch(:disable_visit_errors) do
|
|
17
|
+
default_value(defaults, :disable_visit_errors)
|
|
18
|
+
end)
|
|
19
|
+
@max_depth = depth(values.fetch(:max_depth, default_depth))
|
|
20
|
+
@functions = registry(values.fetch(:functions) { default_value(defaults, :functions) })
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def default_value(defaults, name)
|
|
27
|
+
return defaults.public_send(name) if defaults
|
|
28
|
+
|
|
29
|
+
name == :functions ? Functions.default : false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def merge(positional, keywords)
|
|
33
|
+
raise ArgumentError, "Options must be a Hash" unless positional.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
keywords.each_key do |key|
|
|
36
|
+
raise ArgumentError, "Conflicting option: #{key}" if positional.key?(key) && positional[key] != keywords[key]
|
|
37
|
+
end
|
|
38
|
+
values = positional.merge(keywords)
|
|
39
|
+
raise ArgumentError, "Unknown search options" unless (values.keys - KEYS).empty?
|
|
40
|
+
|
|
41
|
+
values
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def boolean(value)
|
|
45
|
+
case value
|
|
46
|
+
when TrueClass, FalseClass then value
|
|
47
|
+
else raise ArgumentError, "Expected a boolean option"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def depth(value)
|
|
52
|
+
unless value.is_a?(Integer) && (1..128).cover?(value)
|
|
53
|
+
raise ArgumentError, "max_depth must be an integer from 1 through 128"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def registry(value)
|
|
60
|
+
raise ArgumentError, "functions must be a Functions registry" unless value.is_a?(Functions)
|
|
61
|
+
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Options::DEFAULT = Options.new
|
|
67
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Parser
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def prefix_identifier(token)
|
|
9
|
+
node(:field, token.value, [], start: token.start)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def prefix_literal(token)
|
|
13
|
+
node(:literal, token.value, [], start: token.start)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def prefix_current(token)
|
|
17
|
+
node(:current, nil, [], start: token.start)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prefix_group(token)
|
|
21
|
+
inner = expression
|
|
22
|
+
expect(:rparen)
|
|
23
|
+
node(:group, nil, [inner], start: token.start)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prefix_not(token)
|
|
27
|
+
node(:not, nil, [expression(45)], start: token.start)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def prefix_reference(token)
|
|
31
|
+
node(:expression_reference, nil, [expression], start: token.start)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prefix_hash(token)
|
|
35
|
+
pairs = separated(:rbrace) do
|
|
36
|
+
key = expect(:identifier)
|
|
37
|
+
expect(:colon)
|
|
38
|
+
node(:pair, key.value, [expression], start: key.start)
|
|
39
|
+
end
|
|
40
|
+
node(:multi_select_hash, nil, pairs, start: token.start)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def multi_select_list(token)
|
|
44
|
+
items = separated(:rbracket) { expression }
|
|
45
|
+
node(:multi_select_list, nil, items, start: token.start)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def infix_function(left, token)
|
|
49
|
+
unless left.kind == :field && @source.getbyte(left.start) != 34
|
|
50
|
+
syntax!("Function name must be an unquoted identifier", token)
|
|
51
|
+
end
|
|
52
|
+
arguments = separated(:rparen, empty: true) { expression }
|
|
53
|
+
node(:function, left.value, arguments, start: left.start)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def infix_dot(left, _token)
|
|
57
|
+
return node(:value_projection, nil, [left, projection_rhs(20)]) if peek.type == :star && advance
|
|
58
|
+
|
|
59
|
+
sequence(:subexpression, left, dot_rhs(40))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dot_rhs(power)
|
|
63
|
+
return multi_select_list(advance) if peek.type == :lbracket
|
|
64
|
+
|
|
65
|
+
syntax!("Expected a field, function, or multiselect after dot") unless %i[identifier lbrace
|
|
66
|
+
star].include?(peek.type)
|
|
67
|
+
expression(power)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Parser
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def prefix_bracket(token)
|
|
9
|
+
if %i[number colon].include?(peek.type) || (peek.type == :star && @tokens.fetch(@index + 1).type == :rbracket)
|
|
10
|
+
return bracket_body(node(:current))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
multi_select_list(token)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def infix_bracket(left, _token)
|
|
17
|
+
bracket_body(left)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bracket_body(left)
|
|
21
|
+
if peek.type == :star
|
|
22
|
+
advance
|
|
23
|
+
expect(:rbracket)
|
|
24
|
+
return node(:projection, nil, [left, projection_rhs(20)])
|
|
25
|
+
end
|
|
26
|
+
index_or_slice(left)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def index_or_slice(left)
|
|
30
|
+
start = integer if peek.type == :number
|
|
31
|
+
return slice(left, start) if peek.type == :colon
|
|
32
|
+
|
|
33
|
+
syntax!("Expected an array index") unless start
|
|
34
|
+
expect(:rbracket)
|
|
35
|
+
node(:index, start, [left])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def slice(left, start)
|
|
39
|
+
advance
|
|
40
|
+
stop = integer if peek.type == :number
|
|
41
|
+
step = slice_step
|
|
42
|
+
expect(:rbracket)
|
|
43
|
+
raise Errors::InvalidValueError, "Slice step cannot be zero" if step&.zero?
|
|
44
|
+
|
|
45
|
+
base = node(:slice, [start, stop, step], [left])
|
|
46
|
+
node(:projection, nil, [base, projection_rhs(20)])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def slice_step
|
|
50
|
+
return nil unless peek.type == :colon
|
|
51
|
+
|
|
52
|
+
advance
|
|
53
|
+
integer if peek.type == :number
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def integer
|
|
57
|
+
advance.value.to_s.to_i
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prefix_star(_token)
|
|
61
|
+
node(:value_projection, nil, [node(:current), projection_rhs(20)])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def prefix_flatten(token)
|
|
65
|
+
infix_flatten(node(:current), token)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def infix_flatten(left, _token)
|
|
69
|
+
base = node(:flatten, nil, [left])
|
|
70
|
+
node(:projection, nil, [base, projection_rhs(9)])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prefix_filter(token)
|
|
74
|
+
infix_filter(node(:current), token)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def infix_filter(left, _token)
|
|
78
|
+
condition = expression
|
|
79
|
+
expect(:rbracket)
|
|
80
|
+
node(:filter_projection, nil, [left, projection_rhs(21), condition])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def projection_rhs(power)
|
|
84
|
+
return node(:current) if POWER.fetch(peek.type, 0) < 10
|
|
85
|
+
return expression(power) if %i[lbracket filter].include?(peek.type)
|
|
86
|
+
return dot_rhs(power) if peek.type == :dot && advance
|
|
87
|
+
|
|
88
|
+
syntax!("Invalid projection")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Parser
|
|
6
|
+
POWER = {
|
|
7
|
+
pipe: 1, or: 2, and: 3, eq: 5, ne: 5, lt: 5, lte: 5, gt: 5, gte: 5,
|
|
8
|
+
flatten: 9, filter: 21, dot: 40, lbracket: 55, lparen: 60
|
|
9
|
+
}.freeze
|
|
10
|
+
PREFIX = {
|
|
11
|
+
identifier: :prefix_identifier, literal: :prefix_literal, current: :prefix_current,
|
|
12
|
+
lparen: :prefix_group, lbracket: :prefix_bracket, lbrace: :prefix_hash,
|
|
13
|
+
not: :prefix_not, expref: :prefix_reference, star: :prefix_star,
|
|
14
|
+
flatten: :prefix_flatten, filter: :prefix_filter
|
|
15
|
+
}.freeze
|
|
16
|
+
INFIX = {
|
|
17
|
+
dot: :infix_dot, lbracket: :infix_bracket, lparen: :infix_function,
|
|
18
|
+
flatten: :infix_flatten, filter: :infix_filter
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(source, max_depth: 128)
|
|
22
|
+
raise ArgumentError, "max_depth must be an integer from 1 through 128" unless (1..128).cover?(max_depth)
|
|
23
|
+
|
|
24
|
+
lexer = Lexer.new(source, max_depth: max_depth)
|
|
25
|
+
@source = lexer.source
|
|
26
|
+
@tokens = lexer.tokens
|
|
27
|
+
@index = 0
|
|
28
|
+
@level = 0
|
|
29
|
+
@max_depth = max_depth
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse
|
|
33
|
+
result = expression
|
|
34
|
+
expect(:eof)
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def expression(power = 0)
|
|
41
|
+
@level += 1
|
|
42
|
+
syntax!("Expression exceeds nesting limit") if @level > @max_depth + 1
|
|
43
|
+
token = advance
|
|
44
|
+
method = PREFIX[token.type]
|
|
45
|
+
syntax!("Expected an expression", token) unless method
|
|
46
|
+
left = send(method, token)
|
|
47
|
+
left = infix(left, advance) while power < POWER.fetch(peek.type, 0)
|
|
48
|
+
left
|
|
49
|
+
ensure
|
|
50
|
+
@level -= 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def infix(left, token)
|
|
54
|
+
method = INFIX[token.type]
|
|
55
|
+
return send(method, left, token) if method
|
|
56
|
+
|
|
57
|
+
right = expression(POWER.fetch(token.type))
|
|
58
|
+
return sequence(token.type, left, right) if %i[pipe and or].include?(token.type)
|
|
59
|
+
|
|
60
|
+
node(token.type, nil, [left, right])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sequence(kind, left, right)
|
|
64
|
+
children = left.kind == kind ? [*left.children, right] : [left, right]
|
|
65
|
+
node(kind, nil, children)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def node(kind, value = nil, children = [], start: nil)
|
|
69
|
+
result = AST::Node.new(
|
|
70
|
+
kind: kind, value: value, children: children,
|
|
71
|
+
start: start || children.first&.start || previous.start, finish: previous.finish
|
|
72
|
+
)
|
|
73
|
+
syntax!("Expression exceeds nesting limit") if result.depth > @max_depth
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def peek
|
|
78
|
+
@tokens.fetch(@index)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def previous
|
|
82
|
+
@tokens.fetch([@index - 1, 0].max)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def advance
|
|
86
|
+
token = peek
|
|
87
|
+
@index += 1 unless token.type == :eof
|
|
88
|
+
token
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def expect(type)
|
|
92
|
+
token = advance
|
|
93
|
+
syntax!("Expected #{type}, received #{token.type}", token) unless token.type == type
|
|
94
|
+
token
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def separated(terminator, empty: false)
|
|
98
|
+
# @type var result: Array[AST::Node]
|
|
99
|
+
result = []
|
|
100
|
+
unless empty && peek.type == terminator
|
|
101
|
+
loop do
|
|
102
|
+
result << yield
|
|
103
|
+
break unless peek.type == :comma
|
|
104
|
+
|
|
105
|
+
advance
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
expect(terminator)
|
|
109
|
+
result
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def syntax!(message, token = peek)
|
|
113
|
+
raise Errors::SyntaxError.new(message, expression: @source, position: token.start)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
require_relative "parser/expressions"
|
|
119
|
+
require_relative "parser/projections"
|