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
data/docs/guides/search.md
CHANGED
|
@@ -54,3 +54,26 @@ JMESPath.search("{name: user.name, missing: absent}", {user: {name: "Ada"}}) #=>
|
|
|
54
54
|
An omitted negative-slice stop differs from an explicit `-1`.
|
|
55
55
|
A zero slice step raises InvalidValueError.
|
|
56
56
|
Multiselects construct new arrays or hashes and retain explicit nil values.
|
|
57
|
+
|
|
58
|
+
## Search results
|
|
59
|
+
|
|
60
|
+
Use `Expression#try_search` when an integration needs to display library-owned failures without rescuing exceptions.
|
|
61
|
+
The frozen `JMESPath::SearchResult` exposes `success?`, `value`, and `error`.
|
|
62
|
+
Successful null and false remain successful values:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
query = JMESPath.compile("name")
|
|
66
|
+
query.try_search({}).success? #=> true
|
|
67
|
+
query.try_search({}).value #=> nil
|
|
68
|
+
query.try_search({name: false}).value #=> false
|
|
69
|
+
query.try_search({name: false}).error #=> nil
|
|
70
|
+
failure = JMESPath.compile("length(name)").try_search({name: nil})
|
|
71
|
+
failure.success? #=> false
|
|
72
|
+
failure.value #=> nil
|
|
73
|
+
failure.error.is_a?(JMESPath::Errors::InvalidTypeError) #=> true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The container retains selected value identity without copying or freezing the value.
|
|
77
|
+
Failures retain the original exception, metadata, and backtrace without freezing the exception.
|
|
78
|
+
Each call evaluates once, with the same inputs and evaluation options as `search`.
|
|
79
|
+
See [failure ownership](errors.md#failure-results) and [compiled result handling](compilation.md#failure-results).
|
data/lib/jmespath/ast.rb
CHANGED
|
@@ -7,12 +7,13 @@ module JMESPath
|
|
|
7
7
|
class Node
|
|
8
8
|
attr_reader :kind, :value, :children, :start, :finish, :depth
|
|
9
9
|
|
|
10
|
-
def initialize(kind:, value: nil, children: [], start: 0, finish: 0)
|
|
10
|
+
def initialize(kind:, value: nil, children: [], start: 0, finish: 0, synthetic: false) # rubocop:disable Metrics/ParameterLists
|
|
11
11
|
@kind = kind
|
|
12
12
|
@value = self.class.immutable(value)
|
|
13
13
|
@children = children.freeze
|
|
14
14
|
@start = start
|
|
15
15
|
@finish = finish
|
|
16
|
+
@synthetic = synthetic
|
|
16
17
|
@depth = if kind == :literal
|
|
17
18
|
self.class.literal_depth(value)
|
|
18
19
|
else
|
|
@@ -21,6 +22,10 @@ module JMESPath
|
|
|
21
22
|
freeze
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def synthetic?
|
|
26
|
+
@synthetic
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def self.literal_depth(value)
|
|
25
30
|
values = case value
|
|
26
31
|
when Array then value
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module JMESPath
|
|
6
|
+
module CLI
|
|
7
|
+
# Avoid require_exact, which breaks equals values and delimiters on Ruby 3.3.
|
|
8
|
+
# @api private
|
|
9
|
+
class ArgumentParser < OptionParser
|
|
10
|
+
def permute(argv)
|
|
11
|
+
index = 0
|
|
12
|
+
while (argument = argv[index])
|
|
13
|
+
break if argument == "--"
|
|
14
|
+
raise InvalidOption, argument if argument.match?(/\A--(?:raw_output|json_lines)(?:=|\z)/)
|
|
15
|
+
|
|
16
|
+
index += %w[--file --query-file --query_file].include?(argument) ? 2 : 1
|
|
17
|
+
end
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def complete(kind, name, *)
|
|
24
|
+
switch = search(kind, name)
|
|
25
|
+
raise InvalidOption, name unless switch
|
|
26
|
+
|
|
27
|
+
[switch, name]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "argument_parser"
|
|
4
|
+
|
|
5
|
+
module JMESPath
|
|
6
|
+
# Executes queries, validates syntax, or lists built-in functions.
|
|
7
|
+
module CLI
|
|
8
|
+
# @api private
|
|
9
|
+
def self.literal_validation?(argv)
|
|
10
|
+
argv.first == "--validate" && argv.length == 2
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @api private
|
|
14
|
+
def self.parse_arguments(argv)
|
|
15
|
+
if argv.one? && !argv.fetch(0).start_with?("-")
|
|
16
|
+
return [argv.fetch(0), nil, nil, false, false, false, false, false]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
paths = {} # : Hash[String, String]
|
|
20
|
+
arguments = argument_parser(paths).permute(argv)
|
|
21
|
+
check_sources(arguments, paths)
|
|
22
|
+
|
|
23
|
+
[arguments.first, paths["file"], paths["query-file"], paths.key?("validate"), paths.key?("pretty"),
|
|
24
|
+
paths.key?("raw-output"), paths.key?("json-lines"), paths.key?("interactive")]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @api private
|
|
28
|
+
def self.check_sources(arguments, paths)
|
|
29
|
+
if paths.key?("interactive")
|
|
30
|
+
check_interactive(arguments, paths)
|
|
31
|
+
return check_modes(paths)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sources = arguments.length + (paths.key?("query-file") ? 1 : 0)
|
|
35
|
+
raise OptionParser::InvalidArgument, "expected one query source" unless sources == 1
|
|
36
|
+
|
|
37
|
+
check_modes(paths)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api private
|
|
41
|
+
def self.check_modes(paths)
|
|
42
|
+
modes = paths.keys & %w[validate pretty raw-output json-lines interactive]
|
|
43
|
+
raise OptionParser::InvalidArgument, "conflicting modes" if modes.length > 1
|
|
44
|
+
return unless paths.key?("validate")
|
|
45
|
+
return if paths.key?("query-file") && !paths.key?("file")
|
|
46
|
+
|
|
47
|
+
raise OptionParser::InvalidArgument, "validation requires a query file"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @api private
|
|
51
|
+
def self.check_interactive(arguments, paths)
|
|
52
|
+
return if arguments.empty? && paths.key?("file") && !paths.key?("query-file")
|
|
53
|
+
|
|
54
|
+
raise OptionParser::InvalidArgument, "interactive mode requires only a document file"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private
|
|
58
|
+
def self.argument_parser(paths)
|
|
59
|
+
ArgumentParser.new do |options|
|
|
60
|
+
options.on("-h", "--help", "--version", "--functions", "--*-completion-bash", "--*-completion-zsh") do
|
|
61
|
+
raise OptionParser::InvalidOption, "unsupported option combination"
|
|
62
|
+
end
|
|
63
|
+
%w[file query-file].each do |name|
|
|
64
|
+
options.on("--#{name} PATH") do |value|
|
|
65
|
+
raise OptionParser::InvalidOption, "--#{name} repeated" if paths.key?(name)
|
|
66
|
+
|
|
67
|
+
paths[name] = value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
%w[validate pretty raw-output json-lines interactive].each do |name|
|
|
71
|
+
options.on("--#{name}") do
|
|
72
|
+
raise OptionParser::InvalidOption, "--#{name} repeated" if paths.key?(name)
|
|
73
|
+
|
|
74
|
+
paths[name] = ""
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
module FunctionListing
|
|
7
|
+
LEGEND = "Types: any accepts any value; A|B accepts either type.\n" \
|
|
8
|
+
"array<T> requires every element to have type T; array is unrestricted.\n" \
|
|
9
|
+
"expref is an expression reference, written &expression.\n" \
|
|
10
|
+
"T... requires one or more arguments of type T.\n" \
|
|
11
|
+
"Signatures describe declared argument types; runtime checks can be narrower.\n"
|
|
12
|
+
|
|
13
|
+
def self.render(registry)
|
|
14
|
+
entries = registry.names.map do |name|
|
|
15
|
+
metadata = registry.metadata(name) or raise "Missing function metadata: #{name}"
|
|
16
|
+
"#{name}(#{signature(metadata)}) #{metadata.description}\n"
|
|
17
|
+
end
|
|
18
|
+
"#{entries.join}\n#{LEGEND}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def signature(metadata)
|
|
25
|
+
metadata.signature.each_with_index.map do |types, index|
|
|
26
|
+
argument(types, metadata.variadic? && index == metadata.signature.length - 1)
|
|
27
|
+
end.join(", ")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def argument(types, repeated)
|
|
31
|
+
text = types.map { |type| type_name(type) }.join("|")
|
|
32
|
+
return text unless repeated
|
|
33
|
+
|
|
34
|
+
text = "(#{text})" if types.length > 1
|
|
35
|
+
"#{text}..."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def type_name(type)
|
|
39
|
+
case type
|
|
40
|
+
when :array_number then "array<number>"
|
|
41
|
+
when :array_string then "array<string>"
|
|
42
|
+
else type.to_s
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
USAGE = "Usage: jmespath [--pretty | --raw-output | --json-lines] [--file PATH] " \
|
|
6
|
+
"(EXPRESSION | --query-file PATH)\n " \
|
|
7
|
+
"jmespath --validate (EXPRESSION | --query-file PATH)\n jmespath --functions\n " \
|
|
8
|
+
"jmespath --interactive --file PATH"
|
|
9
|
+
HELP = "#{USAGE}\n" \
|
|
10
|
+
"Read one UTF-8 JSON document from --file PATH, or stdin when omitted.\n" \
|
|
11
|
+
"Supply one query as EXPRESSION or --query-file PATH, read as UTF-8 without trimming.\n" \
|
|
12
|
+
"Options may precede or follow EXPRESSION; -- ends option parsing.\n" \
|
|
13
|
+
"Quote spaces: --file 'my document.json'. Use --file=--document.json for option-like names.\n" \
|
|
14
|
+
"Paths are literal: --file - reads the file named -.\n" \
|
|
15
|
+
"Query paths follow the same rules: --query-file - reads the query file named -.\n" \
|
|
16
|
+
"Output is compact JSON by default; --pretty uses two-space indentation and a final newline.\n" \
|
|
17
|
+
"Use --raw-output for literal string results; other results remain compact JSON.\n" \
|
|
18
|
+
"Raw strings retain all their newlines and receive one additional output newline.\n" \
|
|
19
|
+
"Output flags cannot be repeated or combined with each other or --validate.\n" \
|
|
20
|
+
"Use --json-lines for one JSON record per physical input line and one compact result per record.\n" \
|
|
21
|
+
"JSON Lines compiles once before input; empty input succeeds with a valid query.\n" \
|
|
22
|
+
"Blank records are invalid; failures report the input line, retain earlier output, and exit 1.\n" \
|
|
23
|
+
"Memory follows the current record and result; output uses normal buffering.\n" \
|
|
24
|
+
"The --json-lines flag cannot be repeated or combined with --pretty, --raw-output, or --validate.\n" \
|
|
25
|
+
"Function arguments and expression-reference results are validated; failures exit with status 1.\n" \
|
|
26
|
+
"Unknown functions suggest similar registered names.\n" \
|
|
27
|
+
"Query failures include source locations when available.\n" \
|
|
28
|
+
"Use --interactive --file PATH for successive queries against one loaded document.\n" \
|
|
29
|
+
"Enter one query per line; blank lines are ignored. :help shows help; :quit or EOF exits successfully.\n" \
|
|
30
|
+
"Query errors are reported and the session continues; startup or stream failures exit 1.\n" \
|
|
31
|
+
"Results are compact JSON on stdout; prompts and diagnostics use stderr.\n" \
|
|
32
|
+
"Prompts appear only when stdin is a terminal; results are flushed after each query.\n" \
|
|
33
|
+
"Interactive mode requires --file and rejects query sources, other modes, and information flags.\n" \
|
|
34
|
+
"Use --validate to check syntax only, without reading stdin; unknown functions remain valid.\n" \
|
|
35
|
+
"Validate saved queries with --validate --query-file PATH; --file is unavailable during validation.\n" \
|
|
36
|
+
"The two-argument form --validate EXPRESSION always treats EXPRESSION as query text.\n" \
|
|
37
|
+
"Validation exits with status 0 for valid syntax, 1 for query failures, and 2 for usage failures.\n" \
|
|
38
|
+
"Use standalone --functions to list built-in signatures and descriptions without reading stdin.\n" \
|
|
39
|
+
"The --functions flag cannot be repeated or combined with queries, paths, modes, or information flags.\n" \
|
|
40
|
+
"Listing exits with status 0 on success, 1 for output failures, and 2 for usage failures.\n" \
|
|
41
|
+
"Manual Bash and Zsh completion: docs/guides/completion.md.\n#{FunctionListing::LEGEND}".freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
module CLI
|
|
5
|
+
# @api private
|
|
6
|
+
class InteractiveSession
|
|
7
|
+
HELP = "Enter one query per line; blank lines are ignored.\n:help shows help; :quit ends the session."
|
|
8
|
+
|
|
9
|
+
def initialize(data, input:, output:, errors:)
|
|
10
|
+
@data = data
|
|
11
|
+
@input = input
|
|
12
|
+
@output = output
|
|
13
|
+
@errors = errors
|
|
14
|
+
@writing_errors = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
loop do
|
|
19
|
+
prompt
|
|
20
|
+
line = @input.gets
|
|
21
|
+
break unless line
|
|
22
|
+
break if process?(line)
|
|
23
|
+
end
|
|
24
|
+
0
|
|
25
|
+
rescue IOError, SystemCallError => e
|
|
26
|
+
report_stream_failure(e)
|
|
27
|
+
1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def process?(line)
|
|
33
|
+
expression = line.dup.force_encoding(Encoding::UTF_8)
|
|
34
|
+
unless expression.valid_encoding?
|
|
35
|
+
diagnostic("Invalid UTF-8 query")
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
expression = expression.sub(/\r?\n\z/, "")
|
|
39
|
+
command = expression.gsub(/\A[ \t]+|[ \t]+\z/, "")
|
|
40
|
+
command?(command, expression)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def command?(command, expression)
|
|
44
|
+
return true if command == ":quit"
|
|
45
|
+
return false if command.empty?
|
|
46
|
+
|
|
47
|
+
if command == ":help"
|
|
48
|
+
diagnostic(HELP)
|
|
49
|
+
elsif command.start_with?(":")
|
|
50
|
+
diagnostic(FailureDiagnostic.format(Errors::Error.new("Unknown command: #{command}"), nil))
|
|
51
|
+
else
|
|
52
|
+
result = evaluate(expression)
|
|
53
|
+
write_result(result) if result
|
|
54
|
+
end
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def evaluate(expression)
|
|
59
|
+
JSON.generate(JMESPath.search(expression, @data))
|
|
60
|
+
rescue JSON::NestingError
|
|
61
|
+
diagnostic("Invalid JSON result")
|
|
62
|
+
nil
|
|
63
|
+
rescue Errors::Error, JSON::GeneratorError => e
|
|
64
|
+
diagnostic(FailureDiagnostic.format(e, expression))
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def write_result(result)
|
|
69
|
+
@output.puts(result)
|
|
70
|
+
flush(@output)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def diagnostic(text)
|
|
74
|
+
@writing_errors = true
|
|
75
|
+
@errors.puts(text)
|
|
76
|
+
flush(@errors)
|
|
77
|
+
@writing_errors = false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def prompt
|
|
81
|
+
# @type var input: Object & _TerminalReader
|
|
82
|
+
input = @input
|
|
83
|
+
return unless input.respond_to?(:tty?) && input.tty?
|
|
84
|
+
|
|
85
|
+
@writing_errors = true
|
|
86
|
+
# @type var errors: _PromptWriter
|
|
87
|
+
errors = @errors
|
|
88
|
+
errors.print("jmespath> ")
|
|
89
|
+
errors.flush
|
|
90
|
+
@writing_errors = false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def flush(stream)
|
|
94
|
+
# @type var output: Object & _Flushable
|
|
95
|
+
output = stream
|
|
96
|
+
output.flush if output.respond_to?(:flush)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def report_stream_failure(error)
|
|
100
|
+
diagnostic(FailureDiagnostic.format(error, nil)) unless @writing_errors
|
|
101
|
+
rescue IOError, SystemCallError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/jmespath/cli.rb
CHANGED
|
@@ -2,38 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../jmespath"
|
|
4
4
|
require_relative "cli/failure_diagnostic"
|
|
5
|
+
require_relative "cli/arguments"
|
|
6
|
+
require_relative "cli/function_listing"
|
|
7
|
+
require_relative "cli/interactive_session"
|
|
8
|
+
require_relative "cli/help"
|
|
5
9
|
|
|
6
10
|
module JMESPath
|
|
7
|
-
#
|
|
11
|
+
# Executes queries, validates syntax, or lists built-in functions.
|
|
8
12
|
module CLI
|
|
9
|
-
USAGE = "Usage: jmespath EXPRESSION < document.json"
|
|
10
|
-
HELP = "#{USAGE}\n" \
|
|
11
|
-
"Function arguments and expression-reference results are validated; failures exit with status 1.\n" \
|
|
12
|
-
"Unknown functions suggest similar registered names.\n" \
|
|
13
|
-
"Query failures include source locations when available.".freeze
|
|
14
|
-
|
|
15
13
|
# @param argv [Array<String>] command-line arguments
|
|
16
|
-
# @param stdin [IO, StringIO]
|
|
14
|
+
# @param stdin [IO, StringIO] read for documents, gets for JSON Lines or interactive queries; optional tty?
|
|
17
15
|
# @param stdout [IO, StringIO] result stream
|
|
18
16
|
# @param stderr [IO, StringIO] diagnostic stream
|
|
19
17
|
# @return [Integer] zero on success, one on input failure, two on usage failure
|
|
20
18
|
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
21
|
-
return
|
|
19
|
+
return validation(argv, stderr) if literal_validation?(argv)
|
|
20
|
+
return information(argv, stdout) if [%w[--help], %w[-h], %w[--version], %w[--functions]].include?(argv)
|
|
21
|
+
|
|
22
|
+
query(argv, stdin, stdout, stderr)
|
|
23
|
+
rescue OptionParser::ParseError
|
|
24
|
+
stderr.puts(USAGE)
|
|
25
|
+
2
|
|
26
|
+
rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
|
|
27
|
+
failure(e, nil, stderr)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
def self.query(argv, stdin, stdout, stderr)
|
|
32
|
+
expression, path, query_path, validate, pretty, raw_output, json_lines, interactive = parse_arguments(argv)
|
|
33
|
+
return interactive_session(path, stdin, stdout, stderr) if interactive
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
expression = read_query(expression, query_path)
|
|
36
|
+
return validation(["--validate", expression], stderr) if validate
|
|
37
|
+
if json_lines
|
|
38
|
+
# @type var stdin: _LineReader
|
|
39
|
+
return execute_lines(expression, path, stdin, stdout, stderr)
|
|
26
40
|
end
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
|
|
42
|
+
# @type var stdin: _Reader
|
|
43
|
+
execute(expression, read_document(path, stdin), stdout, pretty, raw_output)
|
|
44
|
+
rescue JSON::ParserError, Errors::Error => e
|
|
45
|
+
failure(e, expression, stderr)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
def self.failure(error, expression, errors)
|
|
50
|
+
errors.puts(FailureDiagnostic.format(error, expression))
|
|
51
|
+
1
|
|
52
|
+
rescue IOError, SystemCallError
|
|
53
|
+
1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @api private
|
|
57
|
+
def self.interactive_session(path, input, output, errors)
|
|
58
|
+
# @type var input: _Reader & _LineReader
|
|
59
|
+
data = read_document(path, input)
|
|
60
|
+
InteractiveSession.new(data, input: input, output: output, errors: errors).run
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @api private
|
|
64
|
+
def self.read_query(expression, path)
|
|
65
|
+
return expression || "" unless path
|
|
66
|
+
|
|
67
|
+
File.open(path, "rb:UTF-8:-", &:read)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @api private
|
|
71
|
+
def self.read_document(path, stdin)
|
|
72
|
+
return JSON.parse(stdin.read.to_s) unless path
|
|
73
|
+
|
|
74
|
+
File.open(path, "r:UTF-8") { |file| JSON.parse(file.read) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def self.execute(expression, data, stdout, pretty, raw_output)
|
|
79
|
+
result = JMESPath.search(expression, data)
|
|
80
|
+
output = if raw_output && result.is_a?(String)
|
|
81
|
+
"#{result}\n"
|
|
82
|
+
else
|
|
83
|
+
pretty ? JSON.pretty_generate(result) : JSON.generate(result)
|
|
84
|
+
end
|
|
85
|
+
stdout.puts(output)
|
|
29
86
|
0
|
|
30
|
-
|
|
31
|
-
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @api private
|
|
90
|
+
def self.execute_lines(expression, path, stdin, stdout, stderr)
|
|
91
|
+
compiled = JMESPath.compile(expression)
|
|
92
|
+
return process_lines(compiled, expression, stdin, stdout, stderr) unless path
|
|
93
|
+
|
|
94
|
+
File.open(path, "r:UTF-8") { |file| process_lines(compiled, expression, file, stdout, stderr) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @api private
|
|
98
|
+
def self.process_lines(compiled, expression, input, output, errors)
|
|
99
|
+
line_number = 0
|
|
100
|
+
loop do
|
|
101
|
+
line_number += 1
|
|
102
|
+
line = input.gets
|
|
103
|
+
break unless line
|
|
104
|
+
|
|
105
|
+
line = line.dup.force_encoding(Encoding::UTF_8)
|
|
106
|
+
raise JSON::ParserError, "Invalid UTF-8 input" unless line.valid_encoding?
|
|
107
|
+
|
|
108
|
+
output.puts(JSON.generate(compiled.search(JSON.parse(line))))
|
|
109
|
+
end
|
|
110
|
+
0
|
|
111
|
+
rescue JSON::ParserError, JSON::GeneratorError, Errors::Error, IOError, SystemCallError => e
|
|
112
|
+
errors.puts("Input line #{line_number}:")
|
|
113
|
+
errors.puts(FailureDiagnostic.format(e, expression))
|
|
114
|
+
1
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
def self.validation(argv, errors)
|
|
119
|
+
diagnostic = JMESPath.validate(argv.fetch(1)).first
|
|
120
|
+
return 0 unless diagnostic
|
|
121
|
+
|
|
122
|
+
errors.puts(Diagnostic.format(diagnostic))
|
|
32
123
|
1
|
|
33
124
|
end
|
|
34
125
|
|
|
35
126
|
# @api private
|
|
36
127
|
def self.information(argv, output)
|
|
128
|
+
if argv == ["--functions"]
|
|
129
|
+
output.puts(FunctionListing.render(Functions.default))
|
|
130
|
+
return 0
|
|
131
|
+
end
|
|
132
|
+
|
|
37
133
|
output.puts(argv == ["--version"] ? "jmespath-ruby #{VERSION}" : HELP)
|
|
38
134
|
0
|
|
39
135
|
end
|
data/lib/jmespath/data_access.rb
CHANGED
|
@@ -9,13 +9,15 @@ module JMESPath
|
|
|
9
9
|
mapping = value.is_a?(Hash) ? value : DataAccess.object(value)
|
|
10
10
|
return nil unless mapping
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
FailureCapture.application do
|
|
13
|
+
result = mapping[name]
|
|
14
|
+
return result unless result.nil?
|
|
15
|
+
|
|
16
|
+
mapping.each_key do |key|
|
|
17
|
+
return mapping[key] if key.is_a?(Symbol) && key.name == name
|
|
18
|
+
end
|
|
19
|
+
nil
|
|
17
20
|
end
|
|
18
|
-
nil
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -41,7 +43,7 @@ module JMESPath
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def object(value)
|
|
44
|
-
return value.to_h if value.is_a?(Struct)
|
|
46
|
+
return FailureCapture.application { value.to_h } if value.is_a?(Struct)
|
|
45
47
|
|
|
46
48
|
convert(value, Hash, :to_hash)
|
|
47
49
|
end
|
|
@@ -54,9 +56,10 @@ module JMESPath
|
|
|
54
56
|
|
|
55
57
|
def convert(value, type, method)
|
|
56
58
|
return value if value.is_a?(type)
|
|
57
|
-
return nil unless value.respond_to?(method)
|
|
58
59
|
|
|
59
|
-
result =
|
|
60
|
+
result = FailureCapture.application do
|
|
61
|
+
value.public_send(method) if value.respond_to?(method)
|
|
62
|
+
end
|
|
60
63
|
return result if result.nil? || result.is_a?(type)
|
|
61
64
|
|
|
62
65
|
raise TypeError, "#{method} must return #{type}"
|
data/lib/jmespath/diagnostic.rb
CHANGED
|
@@ -5,7 +5,7 @@ module JMESPath
|
|
|
5
5
|
module Diagnostic
|
|
6
6
|
class << self
|
|
7
7
|
# Format a syntax failure without reading data or printing output.
|
|
8
|
-
# @param error [Errors::SyntaxError] the syntax failure
|
|
8
|
+
# @param error [Errors::SyntaxError, SyntaxDiagnostic] the syntax failure
|
|
9
9
|
# @return [String] UTF-8 diagnostic without a trailing newline
|
|
10
10
|
# @example Format a located failure
|
|
11
11
|
# error = JMESPath::Errors::SyntaxError.new("Bad query", expression: "$", position: 0)
|
|
@@ -30,9 +30,10 @@ module JMESPath
|
|
|
30
30
|
|
|
31
31
|
def source_parts(error)
|
|
32
32
|
source = error.expression
|
|
33
|
-
|
|
33
|
+
position = error.position
|
|
34
|
+
return unless position&.between?(0, source.bytesize) && error.location_known?
|
|
34
35
|
|
|
35
|
-
prefix = source.byteslice(0,
|
|
36
|
+
prefix = source.byteslice(0, position).to_s
|
|
36
37
|
return unless source.valid_encoding? && prefix.valid_encoding?
|
|
37
38
|
|
|
38
39
|
[source.encode(Encoding::UTF_8), prefix.encode(Encoding::UTF_8)]
|
data/lib/jmespath/errors.rb
CHANGED
|
@@ -8,6 +8,20 @@ module JMESPath
|
|
|
8
8
|
# A runtime evaluation failure.
|
|
9
9
|
class RuntimeError < Error; end
|
|
10
10
|
|
|
11
|
+
# A required query result is null.
|
|
12
|
+
class NullResultError < RuntimeError
|
|
13
|
+
# @return [String] the frozen query text
|
|
14
|
+
attr_reader :expression
|
|
15
|
+
|
|
16
|
+
# @param expression [String] the query that returned null
|
|
17
|
+
# @example Inspect a failed requirement
|
|
18
|
+
# JMESPath::Errors::NullResultError.new(expression: "user.name").expression #=> "user.name"
|
|
19
|
+
def initialize(expression:)
|
|
20
|
+
@expression = expression.dup.freeze
|
|
21
|
+
super("Expression returned null")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
11
25
|
# A malformed expression with a byte position and readable coordinates.
|
|
12
26
|
class SyntaxError < Error
|
|
13
27
|
# @return [String] the reason parsing failed, without coordinates
|