jmespath-ruby 0.1.1 → 0.1.2
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/CHANGELOG.md +16 -1
- data/README.md +36 -12
- data/docs/guides/errors.md +190 -2
- data/docs/guides/functions.md +69 -0
- data/lib/jmespath/cli/failure_diagnostic.rb +110 -0
- data/lib/jmespath/cli.rb +7 -2
- data/lib/jmespath/diagnostic.rb +60 -0
- data/lib/jmespath/errors.rb +118 -6
- data/lib/jmespath/evaluation/scalars.rb +2 -1
- data/lib/jmespath/functions/coercions.rb +26 -10
- data/lib/jmespath/functions/collections.rb +49 -25
- data/lib/jmespath/functions/conversion.rb +3 -3
- data/lib/jmespath/functions/function_call.rb +8 -3
- data/lib/jmespath/functions/references.rb +38 -21
- data/lib/jmespath/functions/scalar.rb +37 -20
- data/lib/jmespath/functions/signature.rb +19 -5
- data/lib/jmespath/functions/suggestions.rb +56 -0
- data/lib/jmespath/functions/validation_context.rb +49 -0
- data/lib/jmespath/functions.rb +72 -8
- data/lib/jmespath/plan.rb +13 -7
- data/lib/jmespath/version.rb +1 -1
- data/lib/jmespath.rb +2 -0
- data/sig/jmespath.rbs +38 -1
- metadata +5 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
module Suggestions
|
|
7
|
+
class << self
|
|
8
|
+
def for(name, names)
|
|
9
|
+
names.filter_map { |candidate| ranked(name, candidate) }.sort.first(3).map(&:last)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def ranked(name, candidate)
|
|
15
|
+
threshold = [name.length, candidate.length].max <= 4 ? 1 : 2
|
|
16
|
+
return if (name.length - candidate.length).abs > threshold
|
|
17
|
+
|
|
18
|
+
score = distance(name, candidate)
|
|
19
|
+
[score, candidate] if score <= threshold
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def distance(left, right)
|
|
23
|
+
left = left.chars
|
|
24
|
+
right = right.chars
|
|
25
|
+
previous = (0..right.length).to_a
|
|
26
|
+
older = previous
|
|
27
|
+
left.each_index do |index|
|
|
28
|
+
current = row(left, right, index, previous, older)
|
|
29
|
+
older = previous
|
|
30
|
+
previous = current
|
|
31
|
+
end
|
|
32
|
+
previous.fetch(right.length)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def row(left, right, index, previous, older)
|
|
36
|
+
current = [index + 1]
|
|
37
|
+
right.each_index do |column|
|
|
38
|
+
score = [previous.fetch(column + 1) + 1, current.fetch(column) + 1,
|
|
39
|
+
substitution(left, right, index, column, previous)]
|
|
40
|
+
score << (older.fetch(column - 1) + 1) if swap?(left, right, index, column)
|
|
41
|
+
current << score.min
|
|
42
|
+
end
|
|
43
|
+
current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def substitution(left, right, index, column, previous)
|
|
47
|
+
previous.fetch(column) + (left[index] == right[column] ? 0 : 1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def swap?(left, right, index, column)
|
|
51
|
+
index.positive? && column.positive? && left[index] == right[column - 1] && left[index - 1] == right[column]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
class ValidationContext
|
|
7
|
+
def initialize(name, signature, source_range: nil, expected_types: nil, actual_type: nil)
|
|
8
|
+
@name = name.dup.freeze
|
|
9
|
+
@signature = signature
|
|
10
|
+
@source_range = source_range&.dup&.freeze
|
|
11
|
+
@expected_types = expected_types&.dup&.freeze
|
|
12
|
+
@actual_type = actual_type
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def type_error(message, position:, expected_types:, actual_type:, origin:, stage: :argument) # rubocop:disable Metrics/ParameterLists
|
|
17
|
+
Signature::TypeMismatch.new(
|
|
18
|
+
message,
|
|
19
|
+
**metadata,
|
|
20
|
+
origin: origin, argument_position: position,
|
|
21
|
+
expected_types: @expected_types || expected_types,
|
|
22
|
+
actual_type: @actual_type || actual_type, validation_stage: stage
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def arity_error(message, actual_arity:, origin:)
|
|
27
|
+
Signature::ArityMismatch.new(
|
|
28
|
+
message, **metadata, origin: origin,
|
|
29
|
+
expected_arity: @signature.arguments.length, actual_arity: actual_arity
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def array_element(position)
|
|
34
|
+
with_types(@signature.arguments.fetch(position - 1), actual_type: :array)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_types(types, actual_type: nil)
|
|
38
|
+
self.class.new(@name, @signature, source_range: @source_range, expected_types: types, actual_type: actual_type)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def metadata
|
|
44
|
+
{ function_name: @name, signature: @signature.arguments, variadic: @signature.variadic?,
|
|
45
|
+
source_range: @source_range }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/jmespath/functions.rb
CHANGED
|
@@ -4,7 +4,31 @@ module JMESPath
|
|
|
4
4
|
# Immutable definitions for functions available to an expression.
|
|
5
5
|
class Functions
|
|
6
6
|
# @api private
|
|
7
|
-
Definition = Data.define(:signature, :implementation)
|
|
7
|
+
Definition = Data.define(:signature, :implementation, :description)
|
|
8
|
+
|
|
9
|
+
# Immutable public description of a registered function.
|
|
10
|
+
class Metadata
|
|
11
|
+
# @return [String] function identifier
|
|
12
|
+
attr_reader :name
|
|
13
|
+
# @return [Array<Array<Symbol>>] accepted types for each declared argument
|
|
14
|
+
attr_reader :signature
|
|
15
|
+
# @return [String, nil] optional description
|
|
16
|
+
attr_reader :description
|
|
17
|
+
|
|
18
|
+
# @api private
|
|
19
|
+
def initialize(name, signature, variadic, description)
|
|
20
|
+
@name = name.dup.freeze
|
|
21
|
+
@signature = signature.map { |types| types.dup.freeze }.freeze
|
|
22
|
+
@variadic = variadic
|
|
23
|
+
@description = description&.dup&.freeze
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] whether the final required argument repeats
|
|
28
|
+
def variadic?
|
|
29
|
+
@variadic
|
|
30
|
+
end
|
|
31
|
+
end
|
|
8
32
|
|
|
9
33
|
# Create an empty immutable function registry.
|
|
10
34
|
def initialize
|
|
@@ -19,23 +43,42 @@ module JMESPath
|
|
|
19
43
|
# @param signature [Array<Array<Symbol>>] accepted types for each argument
|
|
20
44
|
# @param variadic [Boolean] repeat the final argument type
|
|
21
45
|
# @param override [Boolean] explicitly replace a definition
|
|
46
|
+
# @param description [String, nil] optional description, replaced on override
|
|
22
47
|
# @yieldparam arguments [Object] evaluated arguments
|
|
23
48
|
# @yieldreturn [Object] the function result
|
|
24
49
|
# @raise [ArgumentError] the definition is invalid or duplicates an existing name
|
|
25
50
|
# @return [Functions] the extended registry
|
|
26
|
-
def with(name, signature:, variadic: false, override: false, &block)
|
|
51
|
+
def with(name, signature:, variadic: false, override: false, description: nil, &block)
|
|
27
52
|
raise ArgumentError, "A function block is required" unless block
|
|
53
|
+
raise ArgumentError, "Invalid function description" unless description.nil? || description.is_a?(String)
|
|
28
54
|
|
|
29
|
-
definition = Definition.new(Signature.new(signature, variadic: variadic), block)
|
|
55
|
+
definition = Definition.new(Signature.new(signature, variadic: variadic), block, description&.dup&.freeze)
|
|
30
56
|
add(name, definition, override: override)
|
|
31
57
|
end
|
|
32
58
|
|
|
33
59
|
# @api private
|
|
34
|
-
def with_builtin(name, signature:, handler:, variadic: false)
|
|
35
|
-
definition = Definition.new(Signature.new(signature, variadic: variadic), handler)
|
|
60
|
+
def with_builtin(name, signature:, handler:, description:, variadic: false)
|
|
61
|
+
definition = Definition.new(Signature.new(signature, variadic: variadic), handler, description.dup.freeze)
|
|
36
62
|
add(name, definition, override: false)
|
|
37
63
|
end
|
|
38
64
|
|
|
65
|
+
# List names in ascending, case-sensitive lexical order.
|
|
66
|
+
# @return [Array<String>] frozen names in a frozen array
|
|
67
|
+
def names
|
|
68
|
+
@definitions.keys.sort.freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Inspect a definition without executing its implementation.
|
|
72
|
+
# @param name [String] function identifier
|
|
73
|
+
# @return [Metadata, nil] immutable metadata, or nil if absent
|
|
74
|
+
def metadata(name)
|
|
75
|
+
definition = @definitions[name]
|
|
76
|
+
return unless definition
|
|
77
|
+
|
|
78
|
+
signature = definition.signature
|
|
79
|
+
Metadata.new(name, signature.arguments, signature.variadic?, definition.description)
|
|
80
|
+
end
|
|
81
|
+
|
|
39
82
|
# Evaluate a registered function with checked arguments.
|
|
40
83
|
# @param name [String] function identifier
|
|
41
84
|
# @param arguments [Array<Object>] evaluated values
|
|
@@ -45,9 +88,19 @@ module JMESPath
|
|
|
45
88
|
# @raise [Errors::InvalidTypeError] argument types are incompatible
|
|
46
89
|
# @raise [Errors::InvalidArityError] the number of arguments is invalid
|
|
47
90
|
def call(name, arguments, disable_visit_errors: false)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
invoke(name, arguments, disable_visit_errors: disable_visit_errors)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private
|
|
95
|
+
def call_with_context(name, arguments, source_range:, disable_visit_errors: nil)
|
|
96
|
+
# Preserve compiled keyword omission separately from the evaluator's explicit false.
|
|
97
|
+
unless method(:call).owner.equal?(Functions)
|
|
98
|
+
return call(name, arguments) if disable_visit_errors.nil?
|
|
99
|
+
|
|
100
|
+
return call(name, arguments, disable_visit_errors: disable_visit_errors)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
invoke(name, arguments, source_range: source_range, disable_visit_errors: !!disable_visit_errors)
|
|
51
104
|
end
|
|
52
105
|
|
|
53
106
|
# @api private
|
|
@@ -57,6 +110,16 @@ module JMESPath
|
|
|
57
110
|
|
|
58
111
|
private
|
|
59
112
|
|
|
113
|
+
def invoke(name, arguments, disable_visit_errors:, source_range: nil)
|
|
114
|
+
definition = @definitions.fetch(name) do
|
|
115
|
+
raise Errors::UnknownFunctionError.new("Unknown function: #{name}", function_name: name,
|
|
116
|
+
suggestions: Suggestions.for(name, names))
|
|
117
|
+
end
|
|
118
|
+
context = ValidationContext.new(name, definition.signature, source_range: source_range)
|
|
119
|
+
invocation = disable_visit_errors ? FunctionCall.new : FunctionCall::STRICT
|
|
120
|
+
invocation.call(definition, name, arguments, disable_visit_errors: disable_visit_errors, context: context)
|
|
121
|
+
end
|
|
122
|
+
|
|
60
123
|
def add(name, definition, override:)
|
|
61
124
|
unless name.is_a?(String) && name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
62
125
|
raise ArgumentError, "Invalid function name"
|
|
@@ -71,3 +134,4 @@ module JMESPath
|
|
|
71
134
|
end
|
|
72
135
|
|
|
73
136
|
require_relative "functions/signature"
|
|
137
|
+
require_relative "functions/validation_context"
|
data/lib/jmespath/plan.rb
CHANGED
|
@@ -104,25 +104,26 @@ module JMESPath
|
|
|
104
104
|
|
|
105
105
|
def function(node)
|
|
106
106
|
name = node.value.to_s
|
|
107
|
+
source_range = (node.start...node.finish)
|
|
107
108
|
unless !@disable_visit_errors && %w[length reverse].include?(name) && node.children.length == 1 &&
|
|
108
109
|
native_function?(name)
|
|
109
|
-
return function_call(name, node.children)
|
|
110
|
+
return function_call(name, node.children, source_range)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
string_function(name, build(node.children.fetch(0)))
|
|
113
|
+
string_function(name, build(node.children.fetch(0)), source_range)
|
|
113
114
|
end
|
|
114
115
|
|
|
115
116
|
def native_function?(name)
|
|
116
117
|
@functions.instance_of?(Functions) && @functions.definition(name).equal?(Functions.default.definition(name))
|
|
117
118
|
end
|
|
118
119
|
|
|
119
|
-
def string_function(name, child)
|
|
120
|
+
def string_function(name, child, source_range)
|
|
120
121
|
lambda do |data|
|
|
121
122
|
value = child.call(data)
|
|
122
123
|
if value.is_a?(String)
|
|
123
124
|
name == "length" ? value.length : value.reverse
|
|
124
125
|
else
|
|
125
|
-
Functions.default.
|
|
126
|
+
Functions.default.call_with_context(name, [value], source_range: source_range)
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
129
|
end
|
|
@@ -132,15 +133,20 @@ module JMESPath
|
|
|
132
133
|
->(data) { evaluator.evaluate(node, data) }
|
|
133
134
|
end
|
|
134
135
|
|
|
135
|
-
def function_call(name, nodes)
|
|
136
|
+
def function_call(name, nodes, source_range)
|
|
136
137
|
arguments = nodes.map { |node| build(node) }.freeze
|
|
137
138
|
functions = @functions
|
|
138
139
|
if @disable_visit_errors
|
|
139
140
|
lambda do |data|
|
|
140
|
-
functions.
|
|
141
|
+
functions.call_with_context(name, arguments.map { |argument| argument.call(data) },
|
|
142
|
+
source_range: source_range, disable_visit_errors: true)
|
|
141
143
|
end
|
|
142
144
|
else
|
|
143
|
-
|
|
145
|
+
lambda do |data|
|
|
146
|
+
functions.call_with_context(name, arguments.map do |argument|
|
|
147
|
+
argument.call(data)
|
|
148
|
+
end, source_range: source_range)
|
|
149
|
+
end
|
|
144
150
|
end
|
|
145
151
|
end
|
|
146
152
|
|
data/lib/jmespath/version.rb
CHANGED
data/lib/jmespath.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "jmespath/version"
|
|
4
4
|
require_relative "jmespath/errors"
|
|
5
|
+
require_relative "jmespath/diagnostic"
|
|
5
6
|
require_relative "jmespath/token"
|
|
6
7
|
require_relative "jmespath/lexer"
|
|
7
8
|
require_relative "jmespath/expression_reference"
|
|
@@ -10,6 +11,7 @@ require_relative "jmespath/types"
|
|
|
10
11
|
require_relative "jmespath/ast"
|
|
11
12
|
require_relative "jmespath/parser"
|
|
12
13
|
require_relative "jmespath/functions"
|
|
14
|
+
require_relative "jmespath/functions/suggestions"
|
|
13
15
|
require_relative "jmespath/evaluator"
|
|
14
16
|
require_relative "jmespath/functions/scalar"
|
|
15
17
|
require_relative "jmespath/functions/collections"
|
data/sig/jmespath.rbs
CHANGED
|
@@ -12,20 +12,48 @@ module JMESPath
|
|
|
12
12
|
class RuntimeError < Error
|
|
13
13
|
end
|
|
14
14
|
class SyntaxError < Error
|
|
15
|
+
attr_reader reason: String
|
|
15
16
|
attr_reader expression: String
|
|
16
17
|
attr_reader position: Integer
|
|
17
18
|
def initialize: (String, ?expression: String, ?position: Integer) -> void
|
|
19
|
+
def location_known?: () -> bool
|
|
18
20
|
def line: () -> Integer
|
|
19
21
|
def column: () -> Integer
|
|
20
22
|
end
|
|
21
23
|
class InvalidTypeError < RuntimeError
|
|
24
|
+
include FunctionFailure
|
|
25
|
+
attr_reader argument_position: Integer?
|
|
26
|
+
attr_reader expected_types: Array[Symbol]?
|
|
27
|
+
attr_reader actual_type: Symbol?
|
|
28
|
+
attr_reader validation_stage: Symbol?
|
|
29
|
+
def initialize: (?String?, ?argument_position: Integer?, ?expected_types: Array[Symbol]?, ?actual_type: Symbol?, ?validation_stage: Symbol?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
|
|
22
30
|
end
|
|
23
31
|
class InvalidValueError < RuntimeError
|
|
24
32
|
end
|
|
25
33
|
class InvalidArityError < RuntimeError
|
|
34
|
+
include FunctionFailure
|
|
35
|
+
attr_reader expected_arity: Integer?
|
|
36
|
+
attr_reader actual_arity: Integer?
|
|
37
|
+
def initialize: (?String?, ?expected_arity: Integer?, ?actual_arity: Integer?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
|
|
26
38
|
end
|
|
27
39
|
class UnknownFunctionError < RuntimeError
|
|
40
|
+
attr_reader function_name: String?
|
|
41
|
+
attr_reader suggestions: Array[String]
|
|
42
|
+
def initialize: (?String?, ?function_name: String?, ?suggestions: Array[String]) -> void
|
|
28
43
|
end
|
|
44
|
+
|
|
45
|
+
module FunctionFailure
|
|
46
|
+
@variadic: bool?
|
|
47
|
+
attr_reader function_name: String?
|
|
48
|
+
attr_reader signature: Array[Array[Symbol]]?
|
|
49
|
+
attr_reader source_range: Range[Integer]?
|
|
50
|
+
def initialize: (?String?, ?function_name: String?, ?signature: Array[Array[Symbol]]?, ?variadic: bool?, ?source_range: Range[Integer]?) -> void
|
|
51
|
+
def variadic?: () -> bool?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module Diagnostic
|
|
56
|
+
def self.format: (Errors::SyntaxError) -> String
|
|
29
57
|
end
|
|
30
58
|
|
|
31
59
|
class Expression
|
|
@@ -35,9 +63,18 @@ module JMESPath
|
|
|
35
63
|
end
|
|
36
64
|
|
|
37
65
|
class Functions
|
|
66
|
+
class Metadata
|
|
67
|
+
attr_reader name: String
|
|
68
|
+
attr_reader signature: Array[Array[Symbol]]
|
|
69
|
+
attr_reader description: String?
|
|
70
|
+
def variadic?: () -> bool
|
|
71
|
+
end
|
|
72
|
+
|
|
38
73
|
def initialize: () -> void
|
|
39
74
|
def self.default: () -> Functions
|
|
40
|
-
def with: (String, signature: Array[Array[Symbol]], ?variadic: bool, ?override: bool) ?{ (*value) -> value } -> Functions
|
|
75
|
+
def with: (String, signature: Array[Array[Symbol]], ?variadic: bool, ?override: bool, ?description: String?) ?{ (*value) -> value } -> Functions
|
|
76
|
+
def names: () -> Array[String]
|
|
77
|
+
def metadata: (String) -> Metadata?
|
|
41
78
|
def call: (String, Array[value], ?disable_visit_errors: bool) -> value
|
|
42
79
|
end
|
|
43
80
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jmespath-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leonid Svyatov
|
|
@@ -77,7 +77,9 @@ files:
|
|
|
77
77
|
- lib/jmespath/ast.rb
|
|
78
78
|
- lib/jmespath/cache.rb
|
|
79
79
|
- lib/jmespath/cli.rb
|
|
80
|
+
- lib/jmespath/cli/failure_diagnostic.rb
|
|
80
81
|
- lib/jmespath/data_access.rb
|
|
82
|
+
- lib/jmespath/diagnostic.rb
|
|
81
83
|
- lib/jmespath/errors.rb
|
|
82
84
|
- lib/jmespath/evaluation/construction.rb
|
|
83
85
|
- lib/jmespath/evaluation/logic.rb
|
|
@@ -95,6 +97,8 @@ files:
|
|
|
95
97
|
- lib/jmespath/functions/references.rb
|
|
96
98
|
- lib/jmespath/functions/scalar.rb
|
|
97
99
|
- lib/jmespath/functions/signature.rb
|
|
100
|
+
- lib/jmespath/functions/suggestions.rb
|
|
101
|
+
- lib/jmespath/functions/validation_context.rb
|
|
98
102
|
- lib/jmespath/input.rb
|
|
99
103
|
- lib/jmespath/lexer.rb
|
|
100
104
|
- lib/jmespath/normalizer.rb
|