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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -1
  3. data/README.md +177 -4
  4. data/completions/_jmespath +50 -0
  5. data/completions/jmespath.bash +178 -0
  6. data/docs/guides/compilation.md +310 -0
  7. data/docs/guides/completion.md +69 -0
  8. data/docs/guides/errors.md +200 -3
  9. data/docs/guides/functions.md +33 -0
  10. data/docs/guides/search.md +23 -0
  11. data/lib/jmespath/ast.rb +6 -1
  12. data/lib/jmespath/cli/argument_parser.rb +31 -0
  13. data/lib/jmespath/cli/arguments.rb +80 -0
  14. data/lib/jmespath/cli/failure_diagnostic.rb +1 -0
  15. data/lib/jmespath/cli/function_listing.rb +48 -0
  16. data/lib/jmespath/cli/help.rb +43 -0
  17. data/lib/jmespath/cli/interactive_session.rb +106 -0
  18. data/lib/jmespath/cli.rb +112 -16
  19. data/lib/jmespath/data_access.rb +12 -9
  20. data/lib/jmespath/diagnostic.rb +4 -3
  21. data/lib/jmespath/errors.rb +14 -0
  22. data/lib/jmespath/explanation.rb +95 -0
  23. data/lib/jmespath/expression.rb +93 -7
  24. data/lib/jmespath/failure_capture.rb +65 -0
  25. data/lib/jmespath/functions/coercions.rb +12 -5
  26. data/lib/jmespath/functions/conversion.rb +19 -1
  27. data/lib/jmespath/functions/function_call.rb +1 -1
  28. data/lib/jmespath/functions/json_value.rb +50 -0
  29. data/lib/jmespath/functions/references.rb +11 -10
  30. data/lib/jmespath/functions/signature.rb +6 -4
  31. data/lib/jmespath/functions.rb +12 -6
  32. data/lib/jmespath/input.rb +37 -0
  33. data/lib/jmespath/normalizer.rb +22 -5
  34. data/lib/jmespath/parser/projections.rb +5 -5
  35. data/lib/jmespath/parser.rb +2 -2
  36. data/lib/jmespath/plan.rb +17 -2
  37. data/lib/jmespath/search_result.rb +23 -0
  38. data/lib/jmespath/syntax_diagnostic.rb +32 -0
  39. data/lib/jmespath/tracing/collector.rb +109 -0
  40. data/lib/jmespath/tracing/evaluator.rb +113 -0
  41. data/lib/jmespath/tracing/field_lookup.rb +49 -0
  42. data/lib/jmespath/version.rb +1 -1
  43. data/lib/jmespath.rb +43 -8
  44. data/sig/jmespath.rbs +47 -3
  45. metadata +17 -1
@@ -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
@@ -21,6 +21,7 @@ module JMESPath
21
21
 
22
22
  def message(error)
23
23
  return "Invalid JSON input" if error.is_a?(JSON::ParserError)
24
+ return "Invalid JSON result" if error.is_a?(JSON::GeneratorError)
24
25
 
25
26
  details = case error
26
27
  when Errors::InvalidTypeError then type_message(error)
@@ -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
- # Runs one JMESPath query against a JSON document from standard input.
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] source JSON stream
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 information(argv, stdout) if [%w[--help], %w[-h], %w[--version]].include?(argv)
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
- unless argv.length == 1
24
- stderr.puts(USAGE)
25
- return 2
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
- data = JSON.parse(stdin.read.to_s)
28
- stdout.puts(JSON.generate(JMESPath.search(argv.fetch(0), data)))
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
- rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
31
- stderr.puts(FailureDiagnostic.format(e, argv.first))
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
@@ -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
- result = mapping[name]
13
- return result unless result.nil?
14
-
15
- mapping.each_key do |key|
16
- return mapping[key] if key.is_a?(Symbol) && key.name == name
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 = value.public_send(method)
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}"
@@ -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
- return unless error.location_known? && error.position.between?(0, source.bytesize)
33
+ position = error.position
34
+ return unless position&.between?(0, source.bytesize) && error.location_known?
34
35
 
35
- prefix = source.byteslice(0, error.position).to_s
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)]
@@ -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