jmespath-ruby 0.1.2 → 0.1.3
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 +19 -1
- data/README.md +177 -4
- data/completions/_jmespath +50 -0
- data/completions/jmespath.bash +178 -0
- data/docs/guides/compilation.md +310 -0
- data/docs/guides/completion.md +69 -0
- data/docs/guides/errors.md +200 -3
- data/docs/guides/functions.md +33 -0
- data/docs/guides/search.md +23 -0
- data/lib/jmespath/ast.rb +6 -1
- data/lib/jmespath/cli/argument_parser.rb +31 -0
- data/lib/jmespath/cli/arguments.rb +80 -0
- data/lib/jmespath/cli/failure_diagnostic.rb +1 -0
- data/lib/jmespath/cli/function_listing.rb +48 -0
- data/lib/jmespath/cli/help.rb +43 -0
- data/lib/jmespath/cli/interactive_session.rb +106 -0
- data/lib/jmespath/cli.rb +112 -16
- data/lib/jmespath/data_access.rb +12 -9
- data/lib/jmespath/diagnostic.rb +4 -3
- data/lib/jmespath/errors.rb +14 -0
- data/lib/jmespath/explanation.rb +95 -0
- data/lib/jmespath/expression.rb +93 -7
- data/lib/jmespath/failure_capture.rb +65 -0
- data/lib/jmespath/functions/coercions.rb +12 -5
- data/lib/jmespath/functions/conversion.rb +19 -1
- data/lib/jmespath/functions/function_call.rb +1 -1
- data/lib/jmespath/functions/json_value.rb +50 -0
- data/lib/jmespath/functions/references.rb +11 -10
- data/lib/jmespath/functions/signature.rb +6 -4
- data/lib/jmespath/functions.rb +12 -6
- data/lib/jmespath/input.rb +37 -0
- data/lib/jmespath/normalizer.rb +22 -5
- data/lib/jmespath/parser/projections.rb +5 -5
- data/lib/jmespath/parser.rb +2 -2
- data/lib/jmespath/plan.rb +17 -2
- data/lib/jmespath/search_result.rb +23 -0
- data/lib/jmespath/syntax_diagnostic.rb +32 -0
- data/lib/jmespath/tracing/collector.rb +109 -0
- data/lib/jmespath/tracing/evaluator.rb +113 -0
- data/lib/jmespath/tracing/field_lookup.rb +49 -0
- data/lib/jmespath/version.rb +1 -1
- data/lib/jmespath.rb +43 -8
- data/sig/jmespath.rbs +47 -3
- metadata +17 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Explanation
|
|
6
|
+
OPERATORS = { eq: "==", ne: "!=", lt: "<", lte: "<=", gt: ">", gte: ">=" }.freeze
|
|
7
|
+
ROLES = {
|
|
8
|
+
group: "expression", subexpression: "step", pipe: "step", index: "input", slice: "input",
|
|
9
|
+
flatten: "input", projection: %w[input expression].freeze, value_projection: %w[input expression].freeze,
|
|
10
|
+
filter_projection: %w[input condition expression].freeze, expression_reference: "expression",
|
|
11
|
+
function: "argument", and: "operand", or: "operand", not: "operand",
|
|
12
|
+
comparison: %w[left right].freeze, multi_select_list: "element", multi_select_hash: "entry", entry: "expression"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def self.syntax_children(syntax)
|
|
16
|
+
return syntax.children unless syntax.kind == :filter_projection
|
|
17
|
+
|
|
18
|
+
[syntax.children.fetch(0), syntax.children.fetch(2), syntax.children.fetch(1)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.build(source, root)
|
|
22
|
+
# @type var result: explanation
|
|
23
|
+
result = { source: source.encode(Encoding::UTF_8).freeze, root: new.node(root) }
|
|
24
|
+
result.freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@next_id = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def node(syntax)
|
|
32
|
+
id = @next_id
|
|
33
|
+
@next_id += 1
|
|
34
|
+
operation = operation(syntax.kind)
|
|
35
|
+
# @type var range: [Integer, Integer]?
|
|
36
|
+
range = syntax.synthetic? ? nil : [syntax.start, syntax.finish].freeze
|
|
37
|
+
# @type var result: explanation_node
|
|
38
|
+
result = {
|
|
39
|
+
id: id, operation: operation,
|
|
40
|
+
source_range: range,
|
|
41
|
+
details: details(syntax).freeze, children: children(syntax, operation).freeze
|
|
42
|
+
}
|
|
43
|
+
result.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def operation(kind)
|
|
49
|
+
return "comparison" if OPERATORS.key?(kind)
|
|
50
|
+
return "entry" if kind == :pair
|
|
51
|
+
|
|
52
|
+
kind.to_s.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def details(syntax)
|
|
56
|
+
operator = OPERATORS[syntax.kind]
|
|
57
|
+
return { operator: operator } if operator
|
|
58
|
+
|
|
59
|
+
case syntax.kind
|
|
60
|
+
when :field, :function, :pair then { name: syntax.value.to_s.dup.freeze }
|
|
61
|
+
when :literal then { value: snapshot(syntax.value) }
|
|
62
|
+
when :index then { index: Integer(syntax.value) }
|
|
63
|
+
when :slice then slice_details(syntax.value)
|
|
64
|
+
else {}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def slice_details(value)
|
|
69
|
+
start, stop, step = Array(value)
|
|
70
|
+
{ start: snapshot(start), stop: snapshot(stop), step: snapshot(step) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def children(syntax, operation)
|
|
74
|
+
roles = ROLES.fetch(operation.to_sym, [])
|
|
75
|
+
syntax_children(syntax).each_with_index.map do |entry, index|
|
|
76
|
+
# @type var result: explanation_child
|
|
77
|
+
result = { role: roles.is_a?(String) ? roles : roles.fetch(index), node: node(entry) }
|
|
78
|
+
result.freeze
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def syntax_children(syntax)
|
|
83
|
+
self.class.syntax_children(syntax)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def snapshot(value)
|
|
87
|
+
case value
|
|
88
|
+
when Hash then value.to_h { |key, entry| [key.to_s.dup.freeze, snapshot(entry)] }.freeze
|
|
89
|
+
when Array then value.map { |entry| snapshot(entry) }.freeze
|
|
90
|
+
when String then value.dup.freeze
|
|
91
|
+
else value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/jmespath/expression.rb
CHANGED
|
@@ -12,7 +12,7 @@ module JMESPath
|
|
|
12
12
|
@source = source.dup.freeze
|
|
13
13
|
@root = root
|
|
14
14
|
@options = options
|
|
15
|
-
@plan = Plan.build(root, options)
|
|
15
|
+
@plan = Plan.build(root, options) { |native_functions| @native_functions = native_functions }
|
|
16
16
|
@max_depth = max_depth
|
|
17
17
|
freeze
|
|
18
18
|
end
|
|
@@ -23,18 +23,88 @@ module JMESPath
|
|
|
23
23
|
# @param keywords [Hash] keyword evaluation options
|
|
24
24
|
# @return [Object] the query result, including nil
|
|
25
25
|
def search(data, options = Options::EMPTY, **keywords)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
FailureCapture.application do
|
|
27
|
+
if options.equal?(Options::EMPTY) && keywords.empty?
|
|
28
|
+
return @plan.call(data.is_a?(Hash) ? data : Input.load(data))
|
|
29
|
+
end
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
config = Options.new(options, keywords, default_depth: @max_depth, defaults: @options)
|
|
32
|
+
evaluate(Input.load(data), config)
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Search input using the same options and behavior as {#search}.
|
|
35
37
|
# @return [Object] the query result, including nil
|
|
36
38
|
alias call search
|
|
37
39
|
|
|
40
|
+
# Search input and return its value or a library-owned failure.
|
|
41
|
+
# @param data [Object] the same input forms accepted by {#search}
|
|
42
|
+
# @param options [Hash] positional evaluation options
|
|
43
|
+
# @param keywords [Hash] keyword evaluation options
|
|
44
|
+
# @return [SearchResult] frozen outcome retaining the original value or error
|
|
45
|
+
# @raise [ArgumentError] evaluation options are invalid
|
|
46
|
+
# Application callback and conversion exceptions propagate unchanged.
|
|
47
|
+
# @example Distinguish successful null from failure
|
|
48
|
+
# JMESPath.compile("missing").try_search({}).success? #=> true
|
|
49
|
+
# JMESPath.compile("@").try_search(false).value #=> false
|
|
50
|
+
# JMESPath.compile("length(@)").try_search(4).success? #=> false
|
|
51
|
+
def try_search(data, options = Options::EMPTY, **keywords)
|
|
52
|
+
fast = options.equal?(Options::EMPTY) && keywords.empty?
|
|
53
|
+
config = fast ? @options : Options.new(options, keywords, default_depth: @max_depth, defaults: @options)
|
|
54
|
+
FailureCapture.run do
|
|
55
|
+
if fast
|
|
56
|
+
@plan.call(data.is_a?(Hash) ? data : Input.load(data))
|
|
57
|
+
else
|
|
58
|
+
evaluate(Input.load(data), config)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Search input and require a non-null result.
|
|
64
|
+
# @param data [Object] the same input forms accepted by {#search}
|
|
65
|
+
# @param options [Hash] positional evaluation options
|
|
66
|
+
# @param keywords [Hash] keyword evaluation options
|
|
67
|
+
# @return [Object] the query result, excluding nil
|
|
68
|
+
# @raise [Errors::NullResultError] evaluation returned nil
|
|
69
|
+
# @example Require a user's name
|
|
70
|
+
# JMESPath.compile("user.name").fetch({user: {name: "Ada"}}) #=> "Ada"
|
|
71
|
+
def fetch(data, options = Options::EMPTY, **keywords)
|
|
72
|
+
value = search(data, options, **keywords)
|
|
73
|
+
raise Errors::NullResultError.new(expression: source) if value.nil?
|
|
74
|
+
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Explain query structure without evaluating input or resolving functions.
|
|
79
|
+
# @return [Hash] deeply frozen source text and logical operation tree
|
|
80
|
+
# @example Inspect a field chain
|
|
81
|
+
# JMESPath.compile("user.name").explain[:root][:operation] #=> "subexpression"
|
|
82
|
+
def explain
|
|
83
|
+
Explanation.build(source, @root)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Evaluate once with bounded diagnostics for null fields and omitted items.
|
|
87
|
+
# @param data [Object] the same input forms accepted by {#search}
|
|
88
|
+
# @param options [Hash] positional evaluation options
|
|
89
|
+
# @param limit [Integer] positive maximum number of retained events
|
|
90
|
+
# @param keywords [Hash] keyword evaluation options
|
|
91
|
+
# @return [Hash] frozen report with value, events, and truncated
|
|
92
|
+
# @raise [ArgumentError] limit or evaluation options are invalid
|
|
93
|
+
# @example Trace a missing field
|
|
94
|
+
# JMESPath.compile("name").trace({})[:events].first[:type] #=> "field_null"
|
|
95
|
+
def trace(data, options = Options::EMPTY, limit: 1000, **keywords)
|
|
96
|
+
FailureCapture.application do
|
|
97
|
+
raise ArgumentError, "limit must be a positive Integer" unless limit.is_a?(Integer) && limit.positive?
|
|
98
|
+
|
|
99
|
+
fast = options.equal?(Options::EMPTY) && keywords.empty?
|
|
100
|
+
config = fast ? @options : Options.new(options, keywords, default_depth: @max_depth, defaults: @options)
|
|
101
|
+
collector = Tracing::Collector.new(root: @root, limit: limit)
|
|
102
|
+
collector.finish(trace_value(trace_input(data, fast), config, collector))
|
|
103
|
+
ensure
|
|
104
|
+
collector&.close
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
38
108
|
# @api private
|
|
39
109
|
def configured(options)
|
|
40
110
|
return self if options.functions.equal?(Functions.default) && !options.disable_visit_errors
|
|
@@ -45,7 +115,7 @@ module JMESPath
|
|
|
45
115
|
# @api private
|
|
46
116
|
def evaluate(data, config)
|
|
47
117
|
if @root.depth > config.max_depth
|
|
48
|
-
raise Errors::SyntaxError.new("Expression exceeds nesting limit", expression: source)
|
|
118
|
+
raise FailureCapture.owned(Errors::SyntaxError.new("Expression exceeds nesting limit", expression: source))
|
|
49
119
|
end
|
|
50
120
|
|
|
51
121
|
return @plan.call(data) if config.equal?(@options) || configured_for?(config)
|
|
@@ -56,6 +126,22 @@ module JMESPath
|
|
|
56
126
|
|
|
57
127
|
private
|
|
58
128
|
|
|
129
|
+
def trace_input(data, fast)
|
|
130
|
+
return data if fast && data.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
Input.load(data)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def trace_value(data, config, collector)
|
|
136
|
+
if @root.depth > config.max_depth
|
|
137
|
+
raise Errors::SyntaxError.new("Expression exceeds nesting limit", expression: source)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
evaluator = Tracing::Evaluator.new(config: config, collector: collector, root: @root,
|
|
141
|
+
planned: configured_for?(config), native_functions: @native_functions)
|
|
142
|
+
evaluator.evaluate(@root, data)
|
|
143
|
+
end
|
|
144
|
+
|
|
59
145
|
def configured_for?(config)
|
|
60
146
|
config.functions.equal?(@options.functions) && config.disable_visit_errors == @options.disable_visit_errors
|
|
61
147
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class FailureCapture
|
|
6
|
+
KEY = :jmespath_failure_capture
|
|
7
|
+
attr_reader :owned, :excluded
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@owned = {} # : Hash[StandardError, bool]
|
|
11
|
+
.compare_by_identity
|
|
12
|
+
@excluded = {} # : Hash[StandardError, bool]
|
|
13
|
+
.compare_by_identity
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.run(&)
|
|
17
|
+
previous = Thread.current[KEY]
|
|
18
|
+
context = new
|
|
19
|
+
Thread.current[KEY] = context
|
|
20
|
+
begin
|
|
21
|
+
context.result(&)
|
|
22
|
+
ensure
|
|
23
|
+
Thread.current[KEY] = previous
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def result
|
|
28
|
+
SearchResult.new(value: yield, error: nil)
|
|
29
|
+
rescue Errors::Error, JSON::ParserError, IOError, SystemCallError => e
|
|
30
|
+
raise unless owned.key?(e) && !excluded.key?(e)
|
|
31
|
+
|
|
32
|
+
SearchResult.new(value: nil, error: e)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.owned(error)
|
|
36
|
+
context = Thread.current[KEY]
|
|
37
|
+
context.owned[error] = true if context
|
|
38
|
+
error
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.active?
|
|
42
|
+
!Thread.current[KEY].nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.application
|
|
46
|
+
context = Thread.current[KEY]
|
|
47
|
+
return yield unless context
|
|
48
|
+
|
|
49
|
+
Thread.current[KEY] = nil
|
|
50
|
+
begin
|
|
51
|
+
yield
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
context.excluded[e] = true
|
|
54
|
+
raise
|
|
55
|
+
ensure
|
|
56
|
+
Thread.current[KEY] = context
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.excluded?(error)
|
|
61
|
+
context = Thread.current[KEY]
|
|
62
|
+
context ? context.excluded.key?(error) : false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -13,25 +13,32 @@ module JMESPath
|
|
|
13
13
|
return value if value.is_a?(Integer)
|
|
14
14
|
return value if value.is_a?(Float) && value.finite?
|
|
15
15
|
|
|
16
|
-
raise type_failure(value, "Expected a finite number", [:number], context, position, stage)
|
|
16
|
+
raise FailureCapture.owned(type_failure(value, "Expected a finite number", [:number], context, position, stage))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def string(value, context: nil, position: nil)
|
|
20
|
-
DataAccess.string(value) || raise(
|
|
20
|
+
DataAccess.string(value) || raise(FailureCapture.owned(
|
|
21
|
+
type_failure(value, "Expected a string", [:string], context, position)
|
|
22
|
+
))
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def array(value, context: nil, position: nil)
|
|
24
|
-
DataAccess.array(value) || raise(
|
|
26
|
+
DataAccess.array(value) || raise(FailureCapture.owned(
|
|
27
|
+
type_failure(value, "Expected an array", [:array], context, position)
|
|
28
|
+
))
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def reference(value, context: nil, position: nil)
|
|
28
32
|
return value if value.is_a?(ExpressionReference)
|
|
29
33
|
|
|
30
|
-
raise type_failure(value, "Expected an expression reference", [:expref], context,
|
|
34
|
+
raise FailureCapture.owned(type_failure(value, "Expected an expression reference", [:expref], context,
|
|
35
|
+
position))
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def object(value, context: nil, position: nil)
|
|
34
|
-
DataAccess.object(value) || raise(
|
|
39
|
+
DataAccess.object(value) || raise(FailureCapture.owned(
|
|
40
|
+
type_failure(value, "Expected an object", [:object], context, position)
|
|
41
|
+
))
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
private
|
|
@@ -13,11 +13,27 @@ module JMESPath
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def to_string(value, **)
|
|
16
|
-
DataAccess.string(value) ||
|
|
16
|
+
DataAccess.string(value) || generate_json(normalize_json(value))
|
|
17
17
|
rescue JSON::NestingError
|
|
18
|
+
raise if FailureCapture.active?
|
|
19
|
+
|
|
18
20
|
raise Errors::InvalidValueError, "Value is too deeply nested to serialize"
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
def generate_json(value)
|
|
24
|
+
JSON.generate(value)
|
|
25
|
+
rescue JSON::NestingError => e
|
|
26
|
+
raise if FailureCapture.excluded?(e)
|
|
27
|
+
|
|
28
|
+
raise FailureCapture.owned(Errors::InvalidValueError.new("Value is too deeply nested to serialize"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_json(value)
|
|
32
|
+
return Types.normalize(value) unless FailureCapture.active?
|
|
33
|
+
|
|
34
|
+
Normalizer.new(key: ->(key) { JSONValue.key(key) }) { |scalar| JSONValue.wrap(scalar) }.call(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
21
37
|
def to_number(value, **)
|
|
22
38
|
return value if Types.number?(value)
|
|
23
39
|
|
|
@@ -30,3 +46,5 @@ module JMESPath
|
|
|
30
46
|
end
|
|
31
47
|
end
|
|
32
48
|
end
|
|
49
|
+
|
|
50
|
+
require_relative "json_value"
|
|
@@ -17,7 +17,7 @@ module JMESPath
|
|
|
17
17
|
if implementation.is_a?(Symbol)
|
|
18
18
|
send(implementation, *arguments, context: context)
|
|
19
19
|
else
|
|
20
|
-
implementation.call(*arguments)
|
|
20
|
+
FailureCapture.application { implementation.call(*arguments) }
|
|
21
21
|
end
|
|
22
22
|
rescue Signature::TypeMismatch, Signature::ArityMismatch => e
|
|
23
23
|
raise unless disable_visit_errors && e.origin.equal?(self)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
class Functions
|
|
5
|
+
# @api private
|
|
6
|
+
class JSONValue
|
|
7
|
+
def self.wrap(value)
|
|
8
|
+
case value
|
|
9
|
+
when NilClass, TrueClass, FalseClass, Integer, Float then value
|
|
10
|
+
when String then Kernel.instance_method(:instance_of?).bind_call(value, String) ? value : new(value)
|
|
11
|
+
else new(value)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.key(value)
|
|
16
|
+
case value
|
|
17
|
+
when Symbol then value
|
|
18
|
+
when String then JSONKey.new(value)
|
|
19
|
+
else new(value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(value)
|
|
24
|
+
@value = value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_json(state)
|
|
28
|
+
FailureCapture.application do
|
|
29
|
+
@value.respond_to?(:to_json) ? @value.to_json(state) : state.generate(String.new(@value.to_s))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
FailureCapture.application { @value.to_s }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @api private
|
|
39
|
+
class JSONKey < String
|
|
40
|
+
def initialize(value)
|
|
41
|
+
super
|
|
42
|
+
@value = value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s
|
|
46
|
+
FailureCapture.application { @value.to_s }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -58,18 +58,19 @@ module JMESPath
|
|
|
58
58
|
def checked_type(value, expected, context:)
|
|
59
59
|
type = Types.name(value)
|
|
60
60
|
unless SORTABLE_TYPES.include?(type)
|
|
61
|
-
raise context.type_error(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
raise FailureCapture.owned(context.type_error(
|
|
62
|
+
"Expression keys must be numbers or strings",
|
|
63
|
+
position: 2, expected_types: expected ? [expected] : SORTABLE_TYPES,
|
|
64
|
+
actual_type: type,
|
|
65
|
+
origin: validation_origin, stage: :expression_result
|
|
66
|
+
))
|
|
66
67
|
end
|
|
67
68
|
if expected && expected != type
|
|
68
|
-
raise context.type_error(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
raise FailureCapture.owned(context.type_error(
|
|
70
|
+
"Expression keys must have one type",
|
|
71
|
+
position: 2, expected_types: [expected], actual_type: type,
|
|
72
|
+
origin: validation_origin, stage: :expression_result
|
|
73
|
+
))
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
type
|
|
@@ -44,8 +44,8 @@ module JMESPath
|
|
|
44
44
|
def validate!(values, name, origin: nil, context: nil)
|
|
45
45
|
context ||= ValidationContext.new(name, self)
|
|
46
46
|
unless valid_arity?(values.length)
|
|
47
|
-
raise context.arity_error("Invalid number of arguments for #{name}",
|
|
48
|
-
|
|
47
|
+
raise FailureCapture.owned(context.arity_error("Invalid number of arguments for #{name}",
|
|
48
|
+
actual_arity: values.length, origin: origin))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
values.each_with_index do |value, index|
|
|
@@ -60,8 +60,10 @@ module JMESPath
|
|
|
60
60
|
|
|
61
61
|
def reject_type!(value, index, types, name, origin, context) # rubocop:disable Metrics/ParameterLists
|
|
62
62
|
actual_type = Types.name(value)
|
|
63
|
-
raise
|
|
64
|
-
|
|
63
|
+
raise FailureCapture.owned(
|
|
64
|
+
context.type_error("Invalid argument #{index + 1} for #{name}: #{actual_type}",
|
|
65
|
+
position: index + 1, expected_types: types, actual_type: actual_type, origin: origin)
|
|
66
|
+
)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def valid_definition?(arguments)
|
data/lib/jmespath/functions.rb
CHANGED
|
@@ -88,16 +88,20 @@ module JMESPath
|
|
|
88
88
|
# @raise [Errors::InvalidTypeError] argument types are incompatible
|
|
89
89
|
# @raise [Errors::InvalidArityError] the number of arguments is invalid
|
|
90
90
|
def call(name, arguments, disable_visit_errors: false)
|
|
91
|
-
invoke(name, arguments, disable_visit_errors: disable_visit_errors)
|
|
91
|
+
FailureCapture.application { invoke(name, arguments, disable_visit_errors: disable_visit_errors) }
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# @api private
|
|
95
95
|
def call_with_context(name, arguments, source_range:, disable_visit_errors: nil)
|
|
96
96
|
# Preserve compiled keyword omission separately from the evaluator's explicit false.
|
|
97
97
|
unless method(:call).owner.equal?(Functions)
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
return FailureCapture.application do
|
|
99
|
+
if disable_visit_errors.nil?
|
|
100
|
+
call(name, arguments)
|
|
101
|
+
else
|
|
102
|
+
call(name, arguments, disable_visit_errors: disable_visit_errors)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
101
105
|
end
|
|
102
106
|
|
|
103
107
|
invoke(name, arguments, source_range: source_range, disable_visit_errors: !!disable_visit_errors)
|
|
@@ -112,8 +116,10 @@ module JMESPath
|
|
|
112
116
|
|
|
113
117
|
def invoke(name, arguments, disable_visit_errors:, source_range: nil)
|
|
114
118
|
definition = @definitions.fetch(name) do
|
|
115
|
-
raise
|
|
116
|
-
|
|
119
|
+
raise FailureCapture.owned(
|
|
120
|
+
Errors::UnknownFunctionError.new("Unknown function: #{name}", function_name: name,
|
|
121
|
+
suggestions: Suggestions.for(name, names))
|
|
122
|
+
)
|
|
117
123
|
end
|
|
118
124
|
context = ValidationContext.new(name, definition.signature, source_range: source_range)
|
|
119
125
|
invocation = disable_visit_errors ? FunctionCall.new : FunctionCall::STRICT
|
data/lib/jmespath/input.rb
CHANGED
|
@@ -8,6 +8,7 @@ module JMESPath
|
|
|
8
8
|
module Input
|
|
9
9
|
def self.load(data)
|
|
10
10
|
return data if data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Struct)
|
|
11
|
+
return captured_load(data) if FailureCapture.active?
|
|
11
12
|
|
|
12
13
|
case data
|
|
13
14
|
when Pathname then File.open(data, "r", encoding: "UTF-8") { |file| JSON.parse(file.read) }
|
|
@@ -15,5 +16,41 @@ module JMESPath
|
|
|
15
16
|
else data
|
|
16
17
|
end
|
|
17
18
|
end
|
|
19
|
+
|
|
20
|
+
def self.captured_load(data)
|
|
21
|
+
case data
|
|
22
|
+
when Pathname then load_file(data)
|
|
23
|
+
when IO, StringIO
|
|
24
|
+
text = read(data)
|
|
25
|
+
parse(FailureCapture.application { text.to_s })
|
|
26
|
+
else data
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.load_file(data)
|
|
31
|
+
path = FailureCapture.application { File.path(data) }
|
|
32
|
+
File.open(path, "r", encoding: "UTF-8") { |file| parse(read(file)) }
|
|
33
|
+
rescue IOError, SystemCallError => e
|
|
34
|
+
raise FailureCapture.owned(e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.read(data)
|
|
38
|
+
return FailureCapture.application { data.read } unless native_read?(data)
|
|
39
|
+
|
|
40
|
+
data.read
|
|
41
|
+
rescue IOError, SystemCallError => e
|
|
42
|
+
raise FailureCapture.owned(e)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.native_read?(data)
|
|
46
|
+
reader = Kernel.instance_method(:method).bind_call(data, :read)
|
|
47
|
+
[IO, StringIO].include?(reader.owner) && reader.source_location.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.parse(text)
|
|
51
|
+
JSON.parse(text)
|
|
52
|
+
rescue JSON::ParserError => e
|
|
53
|
+
raise FailureCapture.owned(e)
|
|
54
|
+
end
|
|
18
55
|
end
|
|
19
56
|
end
|
data/lib/jmespath/normalizer.rb
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module JMESPath
|
|
4
4
|
# @api private
|
|
5
5
|
class Normalizer
|
|
6
|
-
def initialize
|
|
6
|
+
def initialize(key: nil, &scalar)
|
|
7
7
|
@pending = []
|
|
8
8
|
@active = {}
|
|
9
|
+
@scalar = scalar
|
|
10
|
+
@key = key
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def call(value)
|
|
@@ -26,14 +28,27 @@ module JMESPath
|
|
|
26
28
|
|
|
27
29
|
def visit(value, &assign)
|
|
28
30
|
collection = DataAccess.array(value) || DataAccess.object(value)
|
|
29
|
-
return assign.call(
|
|
31
|
+
return assign.call(scalar(value)) unless collection
|
|
30
32
|
|
|
33
|
+
id = activate(value)
|
|
34
|
+
@pending << -> { @active.delete(id) }
|
|
35
|
+
collection.is_a?(Array) ? array(collection, &assign) : object(collection, &assign)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def scalar(value)
|
|
39
|
+
value = DataAccess.string(value) || value
|
|
40
|
+
transform = @scalar
|
|
41
|
+
transform ? transform.call(value) : value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def activate(value)
|
|
31
45
|
id = value.object_id
|
|
32
|
-
|
|
46
|
+
if @active[id]
|
|
47
|
+
raise FailureCapture.owned(Errors::InvalidValueError.new("Cyclic values cannot be compared or serialized"))
|
|
48
|
+
end
|
|
33
49
|
|
|
34
50
|
@active[id] = true
|
|
35
|
-
|
|
36
|
-
collection.is_a?(Array) ? array(collection, &assign) : object(collection, &assign)
|
|
51
|
+
id
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
def array(values)
|
|
@@ -47,6 +62,8 @@ module JMESPath
|
|
|
47
62
|
# @type var result: Hash[value, value]
|
|
48
63
|
result = values.transform_values { nil }
|
|
49
64
|
yield result
|
|
65
|
+
transform = @key
|
|
66
|
+
@pending << -> { result.transform_keys!(&transform) } if transform
|
|
50
67
|
values.to_a.reverse_each { |key, value| enqueue(value) { |entry| result[key] = entry } }
|
|
51
68
|
end
|
|
52
69
|
end
|
|
@@ -7,7 +7,7 @@ module JMESPath
|
|
|
7
7
|
|
|
8
8
|
def prefix_bracket(token)
|
|
9
9
|
if %i[number colon].include?(peek.type) || (peek.type == :star && @tokens.fetch(@index + 1).type == :rbracket)
|
|
10
|
-
return bracket_body(node(:current))
|
|
10
|
+
return bracket_body(node(:current, synthetic: true))
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
multi_select_list(token)
|
|
@@ -58,11 +58,11 @@ module JMESPath
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def prefix_star(_token)
|
|
61
|
-
node(:value_projection, nil, [node(:current), projection_rhs(20)])
|
|
61
|
+
node(:value_projection, nil, [node(:current, synthetic: true), projection_rhs(20)])
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def prefix_flatten(token)
|
|
65
|
-
infix_flatten(node(:current), token)
|
|
65
|
+
infix_flatten(node(:current, synthetic: true), token)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def infix_flatten(left, _token)
|
|
@@ -71,7 +71,7 @@ module JMESPath
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def prefix_filter(token)
|
|
74
|
-
infix_filter(node(:current), token)
|
|
74
|
+
infix_filter(node(:current, synthetic: true), token)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def infix_filter(left, _token)
|
|
@@ -81,7 +81,7 @@ module JMESPath
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def projection_rhs(power)
|
|
84
|
-
return node(:current) if POWER.fetch(peek.type, 0) < 10
|
|
84
|
+
return node(:current, synthetic: true) if POWER.fetch(peek.type, 0) < 10
|
|
85
85
|
return expression(power) if %i[lbracket filter].include?(peek.type)
|
|
86
86
|
return dot_rhs(power) if peek.type == :dot && advance
|
|
87
87
|
|