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,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
COMPARISONS = { lt: :<, lte: :<=, gt: :>, gte: :>= }.freeze
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def boolean(node, data)
|
|
11
|
+
# @type var result: value
|
|
12
|
+
result = nil
|
|
13
|
+
node.children.each do |child|
|
|
14
|
+
result = evaluate(child, data)
|
|
15
|
+
break if node.kind == :or ? !Types.falsey?(result) : Types.falsey?(result)
|
|
16
|
+
end
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def negation?(node, data)
|
|
21
|
+
Types.falsey?(evaluate(node.children.fetch(0), data))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def equality(node, data)
|
|
25
|
+
left, right = node.children.map { |child| evaluate(child, data) }
|
|
26
|
+
result = Types.equal?(left, right)
|
|
27
|
+
node.kind == :eq ? result : !result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ordering(node, data)
|
|
31
|
+
left, right = node.children.map { |child| evaluate(child, data) }
|
|
32
|
+
type = Types.name(left)
|
|
33
|
+
return nil unless type == Types.name(right) && %i[number string].include?(type)
|
|
34
|
+
|
|
35
|
+
if type == :string
|
|
36
|
+
left = Functions::Coercions.string(left)
|
|
37
|
+
right = Functions::Coercions.string(right)
|
|
38
|
+
end
|
|
39
|
+
left.public_send(COMPARISONS.fetch(node.kind), right)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def projection(node, data)
|
|
9
|
+
values = DataAccess.array(evaluate(node.children.fetch(0), data))
|
|
10
|
+
project(node, values)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def value_projection(node, data)
|
|
14
|
+
mapping = DataAccess.object(evaluate(node.children.fetch(0), data))
|
|
15
|
+
project(node, mapping&.values)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def filter_projection(node, data)
|
|
19
|
+
values = DataAccess.array(evaluate(node.children.fetch(0), data))
|
|
20
|
+
selected = values&.reject { |value| Types.falsey?(evaluate(node.children.fetch(2), value)) }
|
|
21
|
+
return nil unless selected
|
|
22
|
+
|
|
23
|
+
child = node.children.fetch(1)
|
|
24
|
+
selected.map! { |value| evaluate(child, value) }
|
|
25
|
+
selected.compact!
|
|
26
|
+
selected
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def project(node, values)
|
|
30
|
+
return nil unless values
|
|
31
|
+
|
|
32
|
+
child = node.children.fetch(1)
|
|
33
|
+
result = values.map { |value| evaluate(child, value) }
|
|
34
|
+
result.compact!
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def flatten(node, data)
|
|
39
|
+
values = DataAccess.array(evaluate(node.children.fetch(0), data))
|
|
40
|
+
values&.flat_map { |value| DataAccess.array(value) || [value] }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def literal(node, _data)
|
|
9
|
+
copy_literal(node.value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def copy_literal(value)
|
|
13
|
+
case value
|
|
14
|
+
when Array then value.map { |item| copy_literal(item) }
|
|
15
|
+
when Hash then value.transform_values { |item| copy_literal(item) }
|
|
16
|
+
when String then value.dup
|
|
17
|
+
else value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def index(node, data)
|
|
22
|
+
array = DataAccess.array(evaluate(node.children.fetch(0), data))
|
|
23
|
+
position = node.value
|
|
24
|
+
raise TypeError, "Invalid array index" unless position.is_a?(Integer)
|
|
25
|
+
|
|
26
|
+
return nil unless array && position.between?(-array.length, array.length - 1)
|
|
27
|
+
|
|
28
|
+
array[position]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def group(node, data)
|
|
32
|
+
evaluate(node.children.fetch(0), data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sequence(node, data)
|
|
36
|
+
node.children.each { |child| data = evaluate(child, data) }
|
|
37
|
+
data
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reference(node, _data)
|
|
41
|
+
ExpressionReference.new { |value| evaluate(node.children.fetch(0), value) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def function(node, data)
|
|
45
|
+
arguments = node.children.map { |child| evaluate(child, data) }
|
|
46
|
+
@functions.call(node.value.to_s, arguments, disable_visit_errors: @disable_visit_errors)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def slice(node, data)
|
|
9
|
+
values = DataAccess.array(evaluate(node.children.fetch(0), data))
|
|
10
|
+
return nil unless values
|
|
11
|
+
|
|
12
|
+
start, stop, step = Array(node.value).map { |part| part.nil? ? nil : Integer(part) }
|
|
13
|
+
Slice.new(start: start, stop: stop, step: step).apply(values)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @api private
|
|
18
|
+
class Slice
|
|
19
|
+
def initialize(start:, stop:, step:)
|
|
20
|
+
@start = start
|
|
21
|
+
@stop = stop
|
|
22
|
+
@step = step || 1
|
|
23
|
+
raise Errors::InvalidValueError, "Slice step cannot be zero" if @step.zero?
|
|
24
|
+
|
|
25
|
+
@forward = @step.positive?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def apply(values)
|
|
29
|
+
length = values.length
|
|
30
|
+
index = bound(@start, length, @forward ? 0 : length - 1)
|
|
31
|
+
stop = bound(@stop, length, @forward ? length : -1)
|
|
32
|
+
# @type var result: Array[value]
|
|
33
|
+
result = []
|
|
34
|
+
while @forward ? index < stop : index > stop
|
|
35
|
+
result << values[index]
|
|
36
|
+
index += @step
|
|
37
|
+
end
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def bound(value, length, fallback)
|
|
44
|
+
return fallback if value.nil?
|
|
45
|
+
|
|
46
|
+
index = value.negative? ? value + length : value
|
|
47
|
+
@forward ? index.clamp(0, length) : index.clamp(-1, length - 1)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
HANDLERS = {
|
|
7
|
+
literal: :literal, index: :index,
|
|
8
|
+
group: :group, subexpression: :sequence, pipe: :sequence,
|
|
9
|
+
and: :boolean, or: :boolean, not: :negation?,
|
|
10
|
+
eq: :equality, ne: :equality, lt: :ordering, lte: :ordering, gt: :ordering, gte: :ordering,
|
|
11
|
+
multi_select_list: :list, multi_select_hash: :mapping,
|
|
12
|
+
function: :function, expression_reference: :reference,
|
|
13
|
+
projection: :projection, value_projection: :value_projection,
|
|
14
|
+
filter_projection: :filter_projection, flatten: :flatten, slice: :slice
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(functions:, disable_visit_errors: false)
|
|
18
|
+
@functions = functions
|
|
19
|
+
@disable_visit_errors = disable_visit_errors
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def evaluate(node, data)
|
|
24
|
+
case node.kind
|
|
25
|
+
when :field then DataAccess.field(data, node.value.to_s)
|
|
26
|
+
when :current then data
|
|
27
|
+
else send(HANDLERS.fetch(node.kind), node, data)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_relative "evaluation/scalars"
|
|
34
|
+
require_relative "evaluation/logic"
|
|
35
|
+
require_relative "evaluation/construction"
|
|
36
|
+
require_relative "evaluation/projections"
|
|
37
|
+
require_relative "evaluation/slices"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# Immutable parsed syntax that can search multiple input documents.
|
|
5
|
+
class Expression
|
|
6
|
+
DEFAULT_EVALUATOR = Evaluator.new(functions: Functions.default)
|
|
7
|
+
# @return [String] an independent, frozen expression string
|
|
8
|
+
attr_reader :source
|
|
9
|
+
|
|
10
|
+
# @api private
|
|
11
|
+
def initialize(source, root, max_depth: 128, options: Options::DEFAULT)
|
|
12
|
+
@source = source.dup.freeze
|
|
13
|
+
@root = root
|
|
14
|
+
@options = options
|
|
15
|
+
@plan = Plan.build(root, options)
|
|
16
|
+
@max_depth = max_depth
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Search input with this parsed expression.
|
|
21
|
+
# @param data [Object] Ruby values, Pathname, IO, or StringIO
|
|
22
|
+
# @param options [Hash] positional compatibility options
|
|
23
|
+
# @param keywords [Hash] keyword evaluation options
|
|
24
|
+
# @return [Object] the query result, including nil
|
|
25
|
+
def search(data, options = Options::EMPTY, **keywords)
|
|
26
|
+
if options.equal?(Options::EMPTY) && keywords.empty?
|
|
27
|
+
return @plan.call(data.is_a?(Hash) ? data : Input.load(data))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
config = Options.new(options, keywords, default_depth: @max_depth, defaults: @options)
|
|
31
|
+
evaluate(Input.load(data), config)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Search input using the same options and behavior as {#search}.
|
|
35
|
+
# @return [Object] the query result, including nil
|
|
36
|
+
alias call search
|
|
37
|
+
|
|
38
|
+
# @api private
|
|
39
|
+
def configured(options)
|
|
40
|
+
return self if options.functions.equal?(Functions.default) && !options.disable_visit_errors
|
|
41
|
+
|
|
42
|
+
self.class.new(source, @root, max_depth: @max_depth, options: options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @api private
|
|
46
|
+
def evaluate(data, config)
|
|
47
|
+
if @root.depth > config.max_depth
|
|
48
|
+
raise Errors::SyntaxError.new("Expression exceeds nesting limit", expression: source)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return @plan.call(data) if config.equal?(@options) || configured_for?(config)
|
|
52
|
+
|
|
53
|
+
evaluator = Evaluator.new(functions: config.functions, disable_visit_errors: config.disable_visit_errors)
|
|
54
|
+
evaluator.evaluate(@root, data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def configured_for?(config)
|
|
60
|
+
config.functions.equal?(@options.functions) && config.disable_visit_errors == @options.disable_visit_errors
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
module Coercions
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def number(value)
|
|
10
|
+
return value if value.is_a?(Integer)
|
|
11
|
+
return value if value.is_a?(Float) && value.finite?
|
|
12
|
+
|
|
13
|
+
raise Signature::TypeMismatch.new("Expected a finite number", origin: validation_origin)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def string(value)
|
|
17
|
+
DataAccess.string(value) || raise(Signature::TypeMismatch.new("Expected a string", origin: validation_origin))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def array(value)
|
|
21
|
+
DataAccess.array(value) || raise(Signature::TypeMismatch.new("Expected an array", origin: validation_origin))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reference(value)
|
|
25
|
+
return value if value.is_a?(ExpressionReference)
|
|
26
|
+
|
|
27
|
+
raise Signature::TypeMismatch.new("Expected an expression reference", origin: validation_origin)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def object(value)
|
|
31
|
+
DataAccess.object(value) || raise(Signature::TypeMismatch.new("Expected an object", origin: validation_origin))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validation_origin
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# Standard and custom function definitions.
|
|
5
|
+
class Functions
|
|
6
|
+
# @api private
|
|
7
|
+
module Collections
|
|
8
|
+
SIGNATURES = {
|
|
9
|
+
"avg" => [[:array_number]], "sum" => [[:array_number]],
|
|
10
|
+
"min" => [%i[array_number array_string]], "max" => [%i[array_number array_string]],
|
|
11
|
+
"sort" => [%i[array_number array_string]], "join" => [[:string], [:array_string]],
|
|
12
|
+
"keys" => [[:object]], "values" => [[:object]], "merge" => [[:object]],
|
|
13
|
+
"map" => [[:expref], [:array]], "sort_by" => [[:array], [:expref]],
|
|
14
|
+
"min_by" => [[:array], [:expref]], "max_by" => [[:array], [:expref]]
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.register(registry)
|
|
18
|
+
SIGNATURES.reduce(registry) do |result, (name, signature)|
|
|
19
|
+
result.with_builtin(name, signature: signature, handler: name.to_sym, variadic: name == "merge")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def sum(value)
|
|
26
|
+
array(value).sum { |item| number(item) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def avg(value)
|
|
30
|
+
values = array(value)
|
|
31
|
+
number(sum(value)).fdiv(values.length) unless values.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def min(value)
|
|
35
|
+
array(value).min_by { |item| sort_key(item) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def max(value)
|
|
39
|
+
array(value).max_by { |item| sort_key(item) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def sort(value)
|
|
43
|
+
array(value).each_with_index.sort_by do |item, index|
|
|
44
|
+
[sort_key(item), index]
|
|
45
|
+
end.map(&:first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def join(separator, value)
|
|
49
|
+
array(value).map do |item|
|
|
50
|
+
string(item)
|
|
51
|
+
end.join(string(separator))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def keys(value)
|
|
55
|
+
object(value).keys.map(&:to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def values(value)
|
|
59
|
+
object(value).values
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def merge(*values)
|
|
63
|
+
# @type var result: Hash[value, value]
|
|
64
|
+
result = {}
|
|
65
|
+
values.each { |value| result.merge!(object(value)) }
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def sort_key(value)
|
|
70
|
+
DataAccess.string(value) || number(value)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @api private
|
|
75
|
+
DEFAULT = Collections.register(Scalar.register(new))
|
|
76
|
+
|
|
77
|
+
# @return [Functions] immutable standard function definitions
|
|
78
|
+
def self.default
|
|
79
|
+
DEFAULT
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
require_relative "references"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
module Scalar
|
|
7
|
+
NUMBER = /\A[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\z/
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def to_array(value)
|
|
12
|
+
DataAccess.array(value) || [value]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_string(value)
|
|
16
|
+
DataAccess.string(value) || JSON.generate(Types.normalize(value))
|
|
17
|
+
rescue JSON::NestingError
|
|
18
|
+
raise Errors::InvalidValueError, "Value is too deeply nested to serialize"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_number(value)
|
|
22
|
+
return value if Types.number?(value)
|
|
23
|
+
|
|
24
|
+
text = DataAccess.string(value)&.strip
|
|
25
|
+
return nil unless text && NUMBER.match?(text)
|
|
26
|
+
|
|
27
|
+
number = Integer(text, 10, exception: false) || Float(text, exception: false)
|
|
28
|
+
number if Types.number?(number)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
class FunctionCall
|
|
7
|
+
include Scalar
|
|
8
|
+
include Collections
|
|
9
|
+
include Coercions
|
|
10
|
+
|
|
11
|
+
private :number, :string, :array, :reference, :object
|
|
12
|
+
|
|
13
|
+
def call(definition, name, arguments, disable_visit_errors: false)
|
|
14
|
+
definition.signature.validate!(arguments, name, origin: self)
|
|
15
|
+
implementation = definition.implementation
|
|
16
|
+
implementation.is_a?(Symbol) ? send(implementation, *arguments) : implementation.call(*arguments)
|
|
17
|
+
rescue Signature::TypeMismatch, Signature::ArityMismatch => e
|
|
18
|
+
raise unless disable_visit_errors && e.origin.equal?(self)
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
STRICT = new.freeze
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validation_origin
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
module Collections
|
|
7
|
+
SORTABLE_TYPES = %i[number string].freeze
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def map(expression, value)
|
|
12
|
+
reference = reference(expression)
|
|
13
|
+
array(value).map { |item| reference.call(item) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def sort_by(value, expression)
|
|
17
|
+
reference = reference(expression)
|
|
18
|
+
# @type var expected: Symbol?
|
|
19
|
+
expected = nil
|
|
20
|
+
index = 0
|
|
21
|
+
array(value).sort_by do |item|
|
|
22
|
+
key = reference.call(item)
|
|
23
|
+
expected = checked_type(key, expected)
|
|
24
|
+
index += 1
|
|
25
|
+
[sort_key(key), index]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def min_by(value, expression)
|
|
30
|
+
keyed(value, expression).min_by(&:first)&.last
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def max_by(value, expression)
|
|
34
|
+
keyed(value, expression).max_by(&:first)&.last
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def keyed(value, expression)
|
|
38
|
+
reference = reference(expression)
|
|
39
|
+
# @type var expected: Symbol?
|
|
40
|
+
expected = nil
|
|
41
|
+
array(value).map do |item|
|
|
42
|
+
key = reference.call(item)
|
|
43
|
+
expected = checked_type(key, expected)
|
|
44
|
+
[sort_key(key), item]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def checked_type(value, expected)
|
|
49
|
+
type = Types.name(value)
|
|
50
|
+
unless SORTABLE_TYPES.include?(type)
|
|
51
|
+
raise Signature::TypeMismatch.new("Expression keys must be numbers or strings", origin: validation_origin)
|
|
52
|
+
end
|
|
53
|
+
if expected && expected != type
|
|
54
|
+
raise Signature::TypeMismatch.new("Expression keys must have one type",
|
|
55
|
+
origin: validation_origin)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
type
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
module Scalar
|
|
7
|
+
SIGNATURES = {
|
|
8
|
+
"abs" => [[:number]], "ceil" => [[:number]], "floor" => [[:number]],
|
|
9
|
+
"contains" => [%i[array string], [:any]],
|
|
10
|
+
"ends_with" => [[:string], [:string]], "starts_with" => [[:string], [:string]],
|
|
11
|
+
"length" => [%i[string array object]], "reverse" => [%i[string array]],
|
|
12
|
+
"type" => [[:any]], "to_array" => [[:any]], "to_string" => [[:any]],
|
|
13
|
+
"to_number" => [[:any]], "not_null" => [[:any]]
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.register(registry)
|
|
17
|
+
SIGNATURES.reduce(registry) do |result, (name, signature)|
|
|
18
|
+
handler = %w[contains ends_with starts_with].include?(name) ? "#{name}?" : name
|
|
19
|
+
result.with_builtin(name, signature: signature, handler: handler.to_sym, variadic: name == "not_null")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def abs(value)
|
|
26
|
+
number(value).abs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ceil(value)
|
|
30
|
+
number(value).ceil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def floor(value)
|
|
34
|
+
number(value).floor
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def contains?(subject, search)
|
|
38
|
+
text = DataAccess.string(subject)
|
|
39
|
+
return text.include?(string(search)) if text
|
|
40
|
+
|
|
41
|
+
array(subject).any? { |item| Types.equal?(item, search) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ends_with?(text, suffix)
|
|
45
|
+
string(text).end_with?(string(suffix))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def starts_with?(text, prefix)
|
|
49
|
+
string(text).start_with?(string(prefix))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def length(value)
|
|
53
|
+
collection = DataAccess.string(value) || DataAccess.array(value) || object(value)
|
|
54
|
+
collection.length
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reverse(value)
|
|
58
|
+
(DataAccess.string(value) || array(value)).reverse
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def type(value)
|
|
62
|
+
name = Types.name(value)
|
|
63
|
+
name.to_s unless name == :unknown
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def not_null(*values)
|
|
67
|
+
values.find { |value| !value.nil? }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
require_relative "coercions"
|
|
74
|
+
require_relative "conversion"
|