jmespath-ruby 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,50 @@ SyntaxError derives from that base; evaluation errors derive from RuntimeError.
12
12
  | `UnknownFunctionError` | Function absent from the supplied registry |
13
13
 
14
14
  SyntaxError exposes the expression, a zero-based byte position, and one-based line and character column.
15
- This distinguishes byte offsets from Unicode display positions.
15
+ Character columns count Unicode code points, independently of byte offsets and terminal display widths.
16
+
17
+ Use `JMESPath::Diagnostic.format(error)` to get the reason, source coordinates, query line, and caret as a string.
18
+ The formatter returns UTF-8 without a trailing newline and does not print output or read an input document.
19
+ For example, rescue a compilation failure and format it:
20
+
21
+ ```ruby
22
+ error = begin
23
+ JMESPath.compile("items[?active")
24
+ rescue JMESPath::Errors::SyntaxError => failure
25
+ failure
26
+ end
27
+ JMESPath::Diagnostic.format(error) #=> "Expected rbracket, received eof at line 1, column 14\nitems[?active\n ^"
28
+ ```
29
+
30
+ The resulting diagnostic is:
31
+
32
+ ```text
33
+ Expected rbracket, received eof at line 1, column 14
34
+ items[?active
35
+ ^
36
+ ```
37
+
38
+ `error.reason` returns the failure reason without the byte-offset suffix in `error.message`.
39
+ `error.location_known?` reports whether an explicit source position was supplied.
40
+ An omitted position retains the compatibility value zero but does not identify a precise failure location.
41
+ An explicitly supplied zero identifies the beginning of the expression, including an empty expression.
42
+
43
+ For unknown locations, unusable encodings, and invalid byte offsets, the formatter returns only the reason.
44
+ Invalid offsets include negative values, values beyond the expression, and positions within a multibyte character.
45
+ Invalid-encoding failures and compiled-expression nesting failures without a precise location use this fallback.
46
+ End-of-input failures place the caret after the last character, or on an empty final line after a trailing LF.
47
+
48
+ The excerpt expands tabs to four-column stops and escapes other C0 controls and DEL.
49
+ The formatted reason uses the same escaping, including visible escapes for embedded line breaks.
50
+ The stored reason and exception message retain their original text.
51
+ LF separates source lines; a carriage return appears as `\r`.
52
+ Caret indentation follows the rendered prefix, including tab expansion and visible escapes.
53
+ Each Unicode code point counts as one column.
54
+ Combining marks, emoji, and wide characters can therefore display differently from the caret's character-based alignment.
55
+ The formatter adds no colors and preserves exception messages.
56
+ The CLI uses it to render syntax failures.
57
+
58
+ Runtime failures use the error classes below:
16
59
 
17
60
  ```ruby
18
61
  JMESPath.search("length(@)", 7) # raises JMESPath::Errors::InvalidTypeError
@@ -25,6 +68,151 @@ Only library argument-type and arity failures are suppressed by `disable_visit_e
25
68
  Syntax, unknown-function, invalid-value, adapter, and callback failures remain visible.
26
69
  Missing fields and out-of-range indices normally return nil.
27
70
 
71
+ ## Function validation data
72
+
73
+ Library-owned `InvalidTypeError` and `InvalidArityError` failures expose immutable metadata.
74
+ Read these fields directly instead of parsing the message.
75
+
76
+ | Field | Meaning |
77
+ | --- | --- |
78
+ | `function_name` | Identifier of the failing function |
79
+ | `signature` | Complete declared signature as arrays of accepted type symbols |
80
+ | `variadic?` | Whether the final declared argument repeats |
81
+ | `source_range` | Exclusive range of zero-based byte offsets for the function call, or nil for direct calls |
82
+
83
+ Use this Ruby example to inspect a type failure:
84
+
85
+ ```ruby
86
+ error = begin
87
+ JMESPath.search("join(',', names)", {names: [1, 2]})
88
+ rescue JMESPath::Errors::InvalidTypeError => failure
89
+ failure
90
+ end
91
+ error.function_name #=> "join"
92
+ error.signature #=> [[:string], [:array_string]]
93
+ error.argument_position #=> 2
94
+ error.expected_types #=> [:array_string]
95
+ error.actual_type #=> :array
96
+ error.validation_stage #=> :argument
97
+ error.source_range #=> (0...16)
98
+ ```
99
+
100
+ Type errors expose `argument_position`, `expected_types`, `actual_type`, and `validation_stage`.
101
+ Argument positions start at one, including trailing variadic arguments.
102
+ Type symbols match function registration: `:array_number` and `:array_string` describe homogeneous arrays.
103
+ An accepted-types array describes a union; empty arrays satisfy both homogeneous-array constraints.
104
+ For homogeneous-array element failures, `expected_types` retains the declared array constraint and `actual_type` describes the outer array.
105
+ Metadata includes no element values or indices.
106
+
107
+ The declared `signature` describes all arguments; `expected_types` describes the specific failing check.
108
+ For example, `contains` accepts any second argument in its signature but requires a string when searching a string.
109
+ `validation_stage` distinguishes an `:argument` failure from an `:expression_result` failure.
110
+ Functions such as `sort_by` require expression results to be numbers or strings of one consistent type.
111
+ After the first valid result, `expected_types` contains only that result's type.
112
+ An error inside a referenced function identifies that inner function and its source range.
113
+
114
+ Arity errors expose `expected_arity` and `actual_arity`.
115
+ `expected_arity` is exact unless `variadic?` is true, when it is the minimum count.
116
+ Use this Ruby example for a direct call without source context:
117
+
118
+ ```ruby
119
+ error = begin
120
+ JMESPath::Functions.default.call("merge", [])
121
+ rescue JMESPath::Errors::InvalidArityError => failure
122
+ failure
123
+ end
124
+ error.function_name #=> "merge"
125
+ error.expected_arity #=> 1
126
+ error.actual_arity #=> 0
127
+ error.variadic? #=> true
128
+ error.source_range #=> nil
129
+ ```
130
+
131
+ Offsets refer to the original query's bytes, including bytes before multibyte characters.
132
+ Metadata contains no argument values or input documents.
133
+ Diagnostics preserve adapter conversion order and counts.
134
+ `actual_type` is `:unknown` when identifying a more specific type would require another adapter conversion.
135
+ Manually constructed errors can have nil metadata; application exceptions retain their identity, backtrace, and original metadata.
136
+ Custom registry overrides of `Functions#call` retain their dispatch contract and may raise errors without source context.
137
+
138
+ ## Unknown function data
139
+
140
+ Library-owned `UnknownFunctionError` failures expose `function_name` and `suggestions`.
141
+ The name identifies the missing function; suggestions contain up to three ranked names from the active registry.
142
+ The name, suggestion strings, and suggestions array are immutable copies.
143
+ Use these fields instead of parsing the error message.
144
+
145
+ Use this Ruby example to inspect a misspelled function:
146
+
147
+ ```ruby
148
+ error = begin
149
+ JMESPath.search("lenght(name)", {name: "Ada"})
150
+ rescue JMESPath::Errors::UnknownFunctionError => failure
151
+ failure
152
+ end
153
+ error.function_name #=> "lenght"
154
+ error.suggestions #=> ["length"]
155
+ error.message #=> "Unknown function: lenght. Did you mean length?"
156
+ ```
157
+
158
+ Messages retain `Unknown function: NAME` when no similar names exist.
159
+ Otherwise, they append `. Did you mean NAME1, NAME2?` using the ranked suggestions.
160
+ The CLI prints this message and exits with status 1.
161
+ `Diagnostic.format` accepts syntax errors; unknown-function diagnostics use `error.message`.
162
+ See [matching rules](functions.md#unknown-function-suggestions) for thresholds and ranking.
163
+
164
+ Lookup remains part of evaluation and follows argument evaluation.
165
+ Unsuppressed argument failures take precedence over a missing outer function, including callback failures when visit errors are disabled.
166
+ Unknown-function failures are not suppressed by `disable_visit_errors`.
167
+ Callback exceptions retain their identity, backtrace, and metadata.
168
+ Manually constructed errors default to a nil `function_name` and empty `suggestions`; positional message construction remains supported.
169
+
28
170
  Invalid JSON streams raise the JSON parser's error class.
29
171
  Invalid options raise ArgumentError.
30
- The CLI translates expected input failures to status 1 and usage failures to status 2.
172
+
173
+ ## CLI diagnostics
174
+
175
+ Both `jmespath` and `jmespath.rb` print expected input and query failures to stderr and exit with status 1.
176
+ Failures before output starts leave stdout empty.
177
+ Successful queries print one JSON result to stdout.
178
+ Usage failures print usage to stderr and exit with status 2 without reading stdin.
179
+ Help and version requests exit with status 0 without reading stdin.
180
+
181
+ To inspect a function type failure, run this command in a shell:
182
+
183
+ ```sh
184
+ printf '%s\n' '4' | jmespath 'length(@)'
185
+ ```
186
+
187
+ The command exits with status 1 and prints this diagnostic to stderr:
188
+
189
+ ```text
190
+ Invalid argument 1 for length: expected string, array, or object; received number at line 1, column 1
191
+ length(@)
192
+ ^
193
+ ```
194
+
195
+ Syntax failures show the reason, coordinates, query line, and caret through `Diagnostic.format`.
196
+ Function type failures report the argument position, expected types, and received type.
197
+ Expression-reference failures identify the rejected expression result and its argument position.
198
+ Arity failures report the expected and received counts, with `at least` marking a variadic minimum.
199
+
200
+ Type and arity carets identify the start of the failing function call, including nested calls.
201
+ They do not identify the exact invalid argument.
202
+ The CLI uses the formatter's character columns, tab expansion, and visible control escapes.
203
+ Wide characters, emoji, and combining marks can display differently from character-based caret alignment.
204
+ Missing or unusable source ranges omit coordinates, the query line, and the caret.
205
+ Incomplete validation metadata retains the escaped exception message and any usable source location.
206
+ Unknown-function diagnostics retain suggestions; invalid-value failures use message-only diagnostics.
207
+
208
+ Malformed stdin JSON prints `Invalid JSON input` without parser messages or document fragments.
209
+ For example, run this command in a shell:
210
+
211
+ ```sh
212
+ printf '%s\n' '{"private_marker":BROKEN}' | jmespath '@'
213
+ ```
214
+
215
+ The command prints `Invalid JSON input` to stderr, leaves stdout empty, and exits with status 1.
216
+ Stream and operating-system failures retain escaped messages.
217
+ Diagnostics add no colors, application backtraces, or input-document values.
218
+ Unrelated application exceptions propagate.
@@ -5,6 +5,11 @@ Expression references, written as `&expression`, defer evaluation until a functi
5
5
 
6
6
  ## Standard functions
7
7
 
8
+ Validation failures expose the full signature, rejected type, argument position, and optional source range.
9
+ See [function validation data](errors.md#function-validation-data) for examples and nullable fields.
10
+ Checks inside built-ins can impose narrower constraints than their declared signatures.
11
+ For example, `sort_by`, `min_by`, and `max_by` require expression-reference results to have one consistent number or string type.
12
+
8
13
  | Function | Accepted arguments | Result |
9
14
  | --- | --- | --- |
10
15
  | `abs` | number | Absolute value |
@@ -63,3 +68,67 @@ Registration returns a new frozen registry and leaves the original intact.
63
68
  Callbacks receive evaluated Ruby values and may retain selected object identity.
64
69
  Return a supported value and avoid mutating shared input.
65
70
  Callback exceptions propagate with their original object and backtrace.
71
+
72
+ ## Registry inspection
73
+
74
+ Use `names` to list functions in the active registry, including custom registrations and overrides.
75
+ Names use ascending, case-sensitive lexical order.
76
+ An empty registry returns an empty array.
77
+
78
+ Use these calls in Ruby to register a description and inspect the function:
79
+
80
+ ```ruby
81
+ functions = JMESPath::Functions.new.with("double", signature: [[:number]], description: "Double a number.") do |value|
82
+ value * 2
83
+ end
84
+ functions.names #=> ["double"]
85
+ metadata = functions.metadata("double")
86
+ metadata.name #=> "double"
87
+ metadata.signature #=> [[:number]]
88
+ metadata.variadic? #=> false
89
+ metadata.description #=> "Double a number."
90
+ functions.metadata("missing") #=> nil
91
+ JMESPath::Functions.default.metadata("length").description #=> "Character or element count."
92
+ ```
93
+
94
+ `metadata(name)` returns a `JMESPath::Functions::Metadata` record, or `nil` if the function is absent.
95
+
96
+ | Field | Meaning |
97
+ | --- | --- |
98
+ | `name` | Function identifier |
99
+ | `signature` | Accepted type symbols for each declared argument |
100
+ | `variadic?` | Whether the final declared argument repeats and remains required |
101
+ | `description` | Optional string, or `nil` when omitted |
102
+
103
+ Signatures preserve unions and typed arrays such as `[:string, :null]` and `[:array_number]`.
104
+ They describe declared arguments; runtime checks can impose further constraints, such as homogeneous results from a `sort_by` expression.
105
+ Return types are not inferred.
106
+
107
+ All built-ins have descriptions.
108
+ Custom descriptions accept strings, including empty strings, or `nil`.
109
+ Other values raise `ArgumentError`.
110
+ An override replaces all metadata, so omitting its description yields `nil`.
111
+
112
+ Inspection does not invoke function implementations.
113
+ The returned arrays, records, strings, and nested signature arrays are frozen.
114
+ Registration copies supplied strings and signature arrays without freezing the caller's objects.
115
+ Later registrations or changes to supplied inputs cannot alter earlier snapshots.
116
+ Shared registries support concurrent inspection; object identity across inspection calls is not guaranteed.
117
+
118
+ ## Unknown function suggestions
119
+
120
+ An unknown function raises `UnknownFunctionError` with up to three similar names from the active registry.
121
+ Candidates include custom functions; overrides appear once.
122
+ Empty registries and unrelated names yield no suggestions.
123
+ Names from other registries are excluded.
124
+
125
+ Matching is case-sensitive and uses optimal string alignment distance.
126
+ Insertion, deletion, replacement, and an adjacent letter swap each cost one edit.
127
+ The same substring cannot be edited twice, which distinguishes this distance from unrestricted transposition matching.
128
+ The maximum distance is one when the longer name has at most four characters, and two otherwise.
129
+ Candidates are ranked by ascending distance, then ascending lexical order.
130
+
131
+ Suggestions inspect registered names without executing candidate callbacks.
132
+ Function lookup happens during evaluation, after the arguments have been evaluated.
133
+ Compilation does not reject an unavailable function, and suggestions never correct or execute a query automatically.
134
+ See [unknown function data](errors.md#unknown-function-data) for structured fields and messages.
@@ -1,5 +1,8 @@
1
1
  # Performance
2
2
 
3
+ For the current comparison across published Ruby implementations, see [implementation comparison](comparison.md).
4
+ The measurements below preserve earlier revisions, private-node diagnostics, and memory experiments against jmespath.rb.
5
+
3
6
  Compile an expression explicitly when evaluating it against many documents.
4
7
  The convenience cache avoids parsing repeated queries, while explicit compilation also avoids cache lookup.
5
8
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ module CLI
5
+ # @api private
6
+ module FailureDiagnostic
7
+ class << self
8
+ def format(error, expression)
9
+ return JMESPath::Diagnostic.format(error) if error.is_a?(Errors::SyntaxError)
10
+
11
+ position = source_position(error, expression)
12
+ diagnostic = if position
13
+ Errors::SyntaxError.new(message(error), expression: expression || "", position: position)
14
+ else
15
+ Errors::SyntaxError.new(message(error))
16
+ end
17
+ JMESPath::Diagnostic.format(diagnostic)
18
+ end
19
+
20
+ private
21
+
22
+ def message(error)
23
+ return "Invalid JSON input" if error.is_a?(JSON::ParserError)
24
+
25
+ details = case error
26
+ when Errors::InvalidTypeError then type_message(error)
27
+ when Errors::InvalidArityError then arity_message(error)
28
+ end
29
+ details || error.message
30
+ end
31
+
32
+ def type_message(error)
33
+ subject = type_subject(error)
34
+ types = error.expected_types
35
+ actual = error.actual_type
36
+ return unless subject && types && actual
37
+ return if types.empty?
38
+
39
+ "#{subject}: expected #{expected_types(types)}; received #{type_name(actual)}"
40
+ end
41
+
42
+ def type_subject(error)
43
+ name = error.function_name
44
+ position = error.argument_position
45
+ return unless name && position&.positive?
46
+
47
+ case error.validation_stage
48
+ when :argument then "Invalid argument #{position} for #{name}"
49
+ when :expression_result then "Invalid expression result for argument #{position} of #{name}"
50
+ end
51
+ end
52
+
53
+ def arity_message(error)
54
+ name = error.function_name
55
+ expected = error.expected_arity
56
+ actual = error.actual_arity
57
+ return unless name && expected && actual
58
+ return if expected.negative? || actual.negative?
59
+
60
+ count = arity_count(expected, error.variadic?)
61
+ return unless count
62
+
63
+ "Invalid number of arguments for #{name}: expected #{count}; received #{actual}"
64
+ end
65
+
66
+ def arity_count(expected, variadic)
67
+ return if variadic.nil?
68
+
69
+ variadic ? "at least #{expected}" : expected.to_s
70
+ end
71
+
72
+ def expected_types(types)
73
+ names = types.map { |type| type_name(type) }
74
+ return names.join(" or ") if names.length < 3
75
+
76
+ "#{names.take(names.length - 1).join(', ')}, or #{names.last}"
77
+ end
78
+
79
+ def type_name(type)
80
+ case type
81
+ when :array_string then "array of strings"
82
+ when :array_number then "array of numbers"
83
+ else type.to_s
84
+ end
85
+ end
86
+
87
+ def source_position(error, expression)
88
+ return unless expression&.valid_encoding?
89
+ return unless error.is_a?(Errors::InvalidTypeError) || error.is_a?(Errors::InvalidArityError)
90
+
91
+ range = error.source_range
92
+ range.begin if range && valid_range?(range, expression)
93
+ end
94
+
95
+ def valid_range?(range, expression)
96
+ start = range.begin
97
+ finish = range.end
98
+ return false unless start && finish && range.exclude_end?
99
+ return false unless valid_bounds?(start, finish, expression)
100
+
101
+ expression.byteslice(0, start).to_s.valid_encoding? && expression.byteslice(0, finish).to_s.valid_encoding?
102
+ end
103
+
104
+ def valid_bounds?(start, finish, expression)
105
+ start >= 0 && finish > start && finish <= expression.bytesize
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/jmespath/cli.rb CHANGED
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../jmespath"
4
+ require_relative "cli/failure_diagnostic"
4
5
 
5
6
  module JMESPath
6
7
  # Runs one JMESPath query against a JSON document from standard input.
7
8
  module CLI
8
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
9
14
 
10
15
  # @param argv [Array<String>] command-line arguments
11
16
  # @param stdin [IO, StringIO] source JSON stream
@@ -23,13 +28,13 @@ module JMESPath
23
28
  stdout.puts(JSON.generate(JMESPath.search(argv.fetch(0), data)))
24
29
  0
25
30
  rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
26
- stderr.puts(e.message)
31
+ stderr.puts(FailureDiagnostic.format(e, argv.first))
27
32
  1
28
33
  end
29
34
 
30
35
  # @api private
31
36
  def self.information(argv, output)
32
- output.puts(argv == ["--version"] ? "jmespath-ruby #{VERSION}" : USAGE)
37
+ output.puts(argv == ["--version"] ? "jmespath-ruby #{VERSION}" : HELP)
33
38
  0
34
39
  end
35
40
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # Plain-text presentation of query syntax failures.
5
+ module Diagnostic
6
+ class << self
7
+ # Format a syntax failure without reading data or printing output.
8
+ # @param error [Errors::SyntaxError] the syntax failure
9
+ # @return [String] UTF-8 diagnostic without a trailing newline
10
+ # @example Format a located failure
11
+ # error = JMESPath::Errors::SyntaxError.new("Bad query", expression: "$", position: 0)
12
+ # JMESPath::Diagnostic.format(error) #=> "Bad query at line 1, column 1\n$\n^"
13
+ def format(error)
14
+ reason = render(error.reason.encode(Encoding::UTF_8, invalid: :replace, undef: :replace))
15
+ parts = source_parts(error)
16
+ return reason unless parts
17
+
18
+ located(reason, parts[0], parts[1])
19
+ end
20
+
21
+ private
22
+
23
+ def located(reason, source, prefix)
24
+ line = prefix.count("\n") + 1
25
+ line_prefix = prefix.rpartition("\n").last
26
+ excerpt = source.split("\n", -1).fetch(line - 1, "")
27
+ caret = "#{' ' * render(line_prefix).length}^"
28
+ "#{reason} at line #{line}, column #{line_prefix.length + 1}\n#{render(excerpt)}\n#{caret}"
29
+ end
30
+
31
+ def source_parts(error)
32
+ source = error.expression
33
+ return unless error.location_known? && error.position.between?(0, source.bytesize)
34
+
35
+ prefix = source.byteslice(0, error.position).to_s
36
+ return unless source.valid_encoding? && prefix.valid_encoding?
37
+
38
+ [source.encode(Encoding::UTF_8), prefix.encode(Encoding::UTF_8)]
39
+ rescue EncodingError
40
+ nil
41
+ end
42
+
43
+ def render(line)
44
+ column = 0
45
+ line.each_char.with_object(+"") do |character, result|
46
+ text = render_character(character, column)
47
+ result << text
48
+ column += text.length
49
+ end
50
+ end
51
+
52
+ def render_character(character, column)
53
+ return " " * (4 - (column % 4)) if character == "\t"
54
+ return character.dump[1...-1].to_s if character.ord < 32 || character.ord == 127
55
+
56
+ character
57
+ end
58
+ end
59
+ end
60
+ end
@@ -10,6 +10,8 @@ module JMESPath
10
10
 
11
11
  # A malformed expression with a byte position and readable coordinates.
12
12
  class SyntaxError < Error
13
+ # @return [String] the reason parsing failed, without coordinates
14
+ attr_reader :reason
13
15
  # @return [String] the expression text
14
16
  attr_reader :expression
15
17
  # @return [Integer] zero-based byte offset
@@ -18,10 +20,17 @@ module JMESPath
18
20
  # @param message [String] the reason parsing failed
19
21
  # @param expression [String] expression text
20
22
  # @param position [Integer] zero-based byte offset
21
- def initialize(message, expression: "", position: 0)
23
+ def initialize(message, expression: "", position: nil)
24
+ @reason = message.dup.freeze
22
25
  @expression = expression.dup.freeze
23
- @position = position
24
- super("#{message} at byte #{position}")
26
+ @location_known = !position.nil?
27
+ @position = position || 0
28
+ super("#{message} at byte #{@position}")
29
+ end
30
+
31
+ # @return [Boolean] whether the caller supplied a source position
32
+ def location_known?
33
+ @location_known
25
34
  end
26
35
 
27
36
  # @return [Integer] one-based line number
@@ -41,13 +50,116 @@ module JMESPath
41
50
  end
42
51
  end
43
52
 
53
+ # @api private
54
+ module FunctionFailure
55
+ # @return [String, nil] function identifier
56
+ attr_reader :function_name
57
+ # @return [Array<Array<Symbol>>, nil] complete declared signature
58
+ attr_reader :signature
59
+ # @return [Range<Integer>, nil] exclusive byte range of the function call
60
+ attr_reader :source_range
61
+
62
+ # @api private
63
+ def initialize(message = nil, function_name: nil, signature: nil, variadic: nil, source_range: nil)
64
+ @function_name = function_name.dup.freeze
65
+ @signature = signature&.map { |types| types.dup.freeze }&.freeze
66
+ @variadic = variadic
67
+ @source_range = source_range.dup.freeze
68
+ super(message)
69
+ end
70
+
71
+ # @return [Boolean, nil] whether the last declared argument repeats
72
+ def variadic?
73
+ @variadic
74
+ end
75
+ end
76
+
44
77
  # A function received an unsupported value type.
45
- class InvalidTypeError < RuntimeError; end
78
+ # @example Inspect a validation failure
79
+ # error = begin
80
+ # JMESPath.search("length(@)", 4)
81
+ # rescue JMESPath::Errors::InvalidTypeError => failure
82
+ # failure
83
+ # end
84
+ # error.expected_types #=> [:string, :array, :object]
85
+ # error.argument_position #=> 1
86
+ class InvalidTypeError < RuntimeError
87
+ include FunctionFailure
88
+
89
+ # @!attribute [r] function_name
90
+ # @return [String, nil] function identifier
91
+ # @!attribute [r] signature
92
+ # @return [Array<Array<Symbol>>, nil] complete declared signature
93
+ # @!attribute [r] source_range
94
+ # @return [Range<Integer>, nil] exclusive byte range of the function call
95
+ # @!method variadic?
96
+ # @return [Boolean, nil] whether the last declared argument repeats
97
+
98
+ # @return [Integer, nil] one-based actual argument position
99
+ attr_reader :argument_position
100
+ # @return [Array<Symbol>, nil] types accepted by the failing check
101
+ attr_reader :expected_types
102
+ # @return [Symbol, nil] rejected type, including :unknown
103
+ attr_reader :actual_type
104
+ # @return [Symbol, nil] :argument or :expression_result
105
+ attr_reader :validation_stage
106
+
107
+ # @api private
108
+ def initialize(message = nil, argument_position: nil, expected_types: nil, actual_type: nil, # rubocop:disable Metrics/ParameterLists
109
+ validation_stage: nil, **context)
110
+ @argument_position = argument_position
111
+ @expected_types = expected_types&.dup&.freeze
112
+ @actual_type = actual_type
113
+ @validation_stage = validation_stage
114
+ super(message, **context)
115
+ end
116
+ end
117
+
46
118
  # A value is invalid for the requested operation.
47
119
  class InvalidValueError < RuntimeError; end
120
+
48
121
  # A function received an incorrect number of arguments.
49
- class InvalidArityError < RuntimeError; end
122
+ class InvalidArityError < RuntimeError
123
+ include FunctionFailure
124
+
125
+ # @!attribute [r] function_name
126
+ # @return [String, nil] function identifier
127
+ # @!attribute [r] signature
128
+ # @return [Array<Array<Symbol>>, nil] complete declared signature
129
+ # @!attribute [r] source_range
130
+ # @return [Range<Integer>, nil] exclusive byte range of the function call
131
+ # @!method variadic?
132
+ # @return [Boolean, nil] whether the last declared argument repeats
133
+
134
+ # @return [Integer, nil] exact argument count, or minimum when variadic
135
+ attr_reader :expected_arity
136
+ # @return [Integer, nil] actual argument count
137
+ attr_reader :actual_arity
138
+
139
+ # @api private
140
+ def initialize(message = nil, expected_arity: nil, actual_arity: nil, **context)
141
+ @expected_arity = expected_arity
142
+ @actual_arity = actual_arity
143
+ super(message, **context)
144
+ end
145
+ end
146
+
50
147
  # A function name is not registered.
51
- class UnknownFunctionError < RuntimeError; end
148
+ class UnknownFunctionError < RuntimeError
149
+ # @return [String, nil] unregistered function identifier
150
+ attr_reader :function_name
151
+ # @return [Array<String>] up to three similar names from the active registry
152
+ attr_reader :suggestions
153
+
154
+ # @param message [String, nil] failure message
155
+ # @param function_name [String, nil] unregistered function identifier
156
+ # @param suggestions [Array<String>] ranked registered names
157
+ def initialize(message = nil, function_name: nil, suggestions: [])
158
+ @function_name = function_name&.dup&.freeze
159
+ @suggestions = suggestions.map { |name| name.dup.freeze }.freeze
160
+ message = "#{message}. Did you mean #{@suggestions.join(', ')}?" unless @suggestions.empty?
161
+ super(message)
162
+ end
163
+ end
52
164
  end
53
165
  end
@@ -43,7 +43,8 @@ module JMESPath
43
43
 
44
44
  def function(node, data)
45
45
  arguments = node.children.map { |child| evaluate(child, data) }
46
- @functions.call(node.value.to_s, arguments, disable_visit_errors: @disable_visit_errors)
46
+ @functions.call_with_context(node.value.to_s, arguments, source_range: node.start...node.finish,
47
+ disable_visit_errors: @disable_visit_errors)
47
48
  end
48
49
  end
49
50
  end