jmespath-ruby 0.1.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -1
- data/README.md +36 -12
- data/docs/guides/errors.md +190 -2
- data/docs/guides/functions.md +69 -0
- data/lib/jmespath/cli/failure_diagnostic.rb +110 -0
- data/lib/jmespath/cli.rb +7 -2
- data/lib/jmespath/diagnostic.rb +60 -0
- data/lib/jmespath/errors.rb +118 -6
- data/lib/jmespath/evaluation/scalars.rb +2 -1
- data/lib/jmespath/functions/coercions.rb +26 -10
- data/lib/jmespath/functions/collections.rb +49 -25
- data/lib/jmespath/functions/conversion.rb +3 -3
- data/lib/jmespath/functions/function_call.rb +8 -3
- data/lib/jmespath/functions/references.rb +38 -21
- data/lib/jmespath/functions/scalar.rb +37 -20
- data/lib/jmespath/functions/signature.rb +19 -5
- data/lib/jmespath/functions/suggestions.rb +56 -0
- data/lib/jmespath/functions/validation_context.rb +49 -0
- data/lib/jmespath/functions.rb +72 -8
- data/lib/jmespath/plan.rb +13 -7
- data/lib/jmespath/version.rb +1 -1
- data/lib/jmespath.rb +2 -0
- data/sig/jmespath.rbs +38 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9f2db0a8d51af9e3258964210daf3aa1391f97370fca0d0e5610045aad9e012
|
|
4
|
+
data.tar.gz: ec560bc1d5607e14175267d6b1bb82329a3d126686d99393c2cd6a8e7bc62585
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8b8a4b5d663ae5b6ac54d093867c94ea420d836d44f1de5de1b781c21a984277a8211f7a6343392b335d0e451b7f3f749802e0ad910fc0af45d573fb305d2b9
|
|
7
|
+
data.tar.gz: 985c5eddf2e47265eaa74f37afd4ea73094ae84f6a87ec90ec0c0501be1af03b3c46a4d95207be57d42ec47f4b94dc2b632a1e87b5eabfa712e3ba4232753794
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for t
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add `JMESPath::Diagnostic.format` for syntax reasons, source coordinates, query excerpts, and carets.
|
|
13
|
+
- Expose immutable function validation data, including argument positions, expected and actual types, arity, and query byte ranges.
|
|
14
|
+
- Add `Functions#names`, `Functions#metadata`, and optional custom function descriptions for immutable registry inspection.
|
|
15
|
+
- Suggest similar names from the active registry for unknown functions; expose the missing name and ranked suggestions on the error.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Show located CLI syntax, function type, and arity diagnostics; describe validation and suggestions in CLI help.
|
|
20
|
+
- Report malformed CLI input as `Invalid JSON input` without exposing JSON parser messages or document fragments.
|
|
21
|
+
|
|
8
22
|
## [0.1.1] - 2026-11-01
|
|
9
23
|
|
|
10
24
|
### Changed
|
|
@@ -31,6 +45,7 @@ Initial implementation for Ruby 3.3 and later.
|
|
|
31
45
|
The compatibility guide documents intentional corrections and limits.
|
|
32
46
|
Applications that depend on the gem name `jmespath` must change that dependency explicitly.
|
|
33
47
|
|
|
34
|
-
[unreleased]: https://github.com/svyatov/jmespath-ruby/compare/v0.1.
|
|
48
|
+
[unreleased]: https://github.com/svyatov/jmespath-ruby/compare/v0.1.2...HEAD
|
|
49
|
+
[0.1.2]: https://github.com/svyatov/jmespath-ruby/compare/v0.1.1...v0.1.2
|
|
35
50
|
[0.1.1]: https://github.com/svyatov/jmespath-ruby/compare/v0.1.0...v0.1.1
|
|
36
51
|
[0.1.0]: https://github.com/svyatov/jmespath-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Search JSON and Ruby data with reusable queries and typed custom functions.
|
|
|
12
12
|
In your Ruby project, add the gem with Bundler. Run `bundle init` first if the project has no Gemfile.
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
bundle add jmespath-ruby --version "~> 0.1.
|
|
15
|
+
bundle add jmespath-ruby --version "~> 0.1.2"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Save this Ruby example as `example.rb`:
|
|
@@ -33,10 +33,10 @@ On MRI 4.0.9, jmespath-ruby beats each competing implementation on every workloa
|
|
|
33
33
|
|
|
34
34
|
| Implementation | Version | Correct workloads | jmespath-ruby public-search speedup |
|
|
35
35
|
| --- | --- | ---: | ---: |
|
|
36
|
-
| **jmespath-ruby** | **0.1.0** |
|
|
37
|
-
| jmespath (jmespath.rb) | 1.6.2 |
|
|
38
|
-
| BurtPath | 1.1.2 |
|
|
39
|
-
| Jamespath | 0.5.1 |
|
|
36
|
+
| **jmespath-ruby** | **0.1.0** | **18/18** | **Baseline** |
|
|
37
|
+
| jmespath (jmespath.rb) | 1.6.2 | 18/18 | **7.97x** |
|
|
38
|
+
| BurtPath | 1.1.2 | 15/18 | **9.99x** |
|
|
39
|
+
| Jamespath | 0.5.1 | 13/18 | **8.07x** |
|
|
40
40
|
|
|
41
41
|
Speedups use geometric means of median times across each competitor's correctly evaluated workloads.
|
|
42
42
|
Measurements use five rounds of 20,000 searches on an Apple M1 Pro.
|
|
@@ -60,11 +60,11 @@ The [comparison with existing implementations](docs/guides/comparison.md) includ
|
|
|
60
60
|
|
|
61
61
|
| Feature | jmespath-ruby | jmespath | BurtPath | Jamespath | jmespath-jruby |
|
|
62
62
|
| --- | --- | --- | --- | --- | --- |
|
|
63
|
-
| Public query compilation |
|
|
64
|
-
| Documented typed function registry |
|
|
65
|
-
| Packaged RBS signatures |
|
|
66
|
-
| Pure Ruby implementation |
|
|
67
|
-
| Ordinary `JMESPath.search` calls |
|
|
63
|
+
| Public query compilation | Yes | No | No | Yes | Yes |
|
|
64
|
+
| Documented typed function registry | Yes | No | No | No | No |
|
|
65
|
+
| Packaged RBS signatures | Yes | No | No | No | No |
|
|
66
|
+
| Pure Ruby implementation | Yes | Yes | Yes | Yes | No |
|
|
67
|
+
| Ordinary `JMESPath.search` calls | Yes | Yes | Yes | No | No |
|
|
68
68
|
|
|
69
69
|
`jmespath` and BurtPath support reuse through private runtime classes.
|
|
70
70
|
The [full comparison](docs/guides/comparison.md) distinguishes those classes from public compilation APIs.
|
|
@@ -76,7 +76,7 @@ Replace that Gemfile entry:
|
|
|
76
76
|
|
|
77
77
|
```diff
|
|
78
78
|
-gem "jmespath"
|
|
79
|
-
+gem "jmespath-ruby", "~> 0.1.
|
|
79
|
+
+gem "jmespath-ruby", "~> 0.1.2"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Run `bundle install`. Your Ruby code keeps the same entry points:
|
|
@@ -144,6 +144,25 @@ Registration returns a separate immutable registry.
|
|
|
144
144
|
An existing function requires `override: true` to replace it.
|
|
145
145
|
Every argument, including variadic arguments, is validated before the callback runs.
|
|
146
146
|
|
|
147
|
+
Inspect registered functions in Ruby without executing their callbacks:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
JMESPath::Functions.default.names.include?("length") #=> true
|
|
151
|
+
metadata = JMESPath::Functions.default.metadata("length")
|
|
152
|
+
metadata.signature #=> [[:string, :array, :object]]
|
|
153
|
+
metadata.description #=> "Character or element count."
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Use `description:` when registering a custom function to include its description in immutable metadata.
|
|
157
|
+
See [registry inspection](docs/guides/functions.md#registry-inspection) for all fields and override behavior.
|
|
158
|
+
|
|
159
|
+
## Errors and diagnostics
|
|
160
|
+
|
|
161
|
+
Format syntax failures in Ruby with `JMESPath::Diagnostic.format(error)` to get source coordinates, a query excerpt, and a caret.
|
|
162
|
+
Function type and arity errors expose immutable validation data, including the function name, signature, argument counts or types, and available source ranges.
|
|
163
|
+
Unknown-function errors expose the missing name and up to three suggestions from the active registry.
|
|
164
|
+
See [errors and diagnostics](docs/guides/errors.md) for examples, nullable fields, and source-location limits.
|
|
165
|
+
|
|
147
166
|
## Command line
|
|
148
167
|
|
|
149
168
|
Both `jmespath` and `jmespath.rb` accept an expression and a JSON document on standard input.
|
|
@@ -153,7 +172,12 @@ printf '%s\n' '{"user":{"name":"Ada"}}' | jmespath 'user.name'
|
|
|
153
172
|
```
|
|
154
173
|
|
|
155
174
|
The result goes to standard output as JSON.
|
|
156
|
-
|
|
175
|
+
Syntax, function type, and arity failures include source coordinates, a query excerpt, and a caret when available.
|
|
176
|
+
Unknown functions suggest similar registered names.
|
|
177
|
+
Malformed JSON input reports `Invalid JSON input` without including document fragments.
|
|
178
|
+
These failures go to standard error and exit with status 1.
|
|
179
|
+
Usage failures exit with status 2; `--help` and `--version` exit with status 0 without reading input.
|
|
180
|
+
See [CLI diagnostics](docs/guides/errors.md#cli-diagnostics) for examples and fallback behavior.
|
|
157
181
|
|
|
158
182
|
## Guides
|
|
159
183
|
|
data/docs/guides/errors.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
data/docs/guides/functions.md
CHANGED
|
@@ -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.
|
|
@@ -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.
|
|
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}" :
|
|
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
|