jmespath-ruby 0.1.0
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 +7 -0
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +57 -0
- data/LICENSE +21 -0
- data/LICENSES/python-MIT.txt +21 -0
- data/LICENSES/ruby-Apache-2.0.txt +174 -0
- data/README.md +122 -0
- data/SECURITY.md +16 -0
- data/VERSIONING.md +12 -0
- data/docs/guides/compatibility.md +84 -0
- data/docs/guides/compilation.md +63 -0
- data/docs/guides/errors.md +30 -0
- data/docs/guides/functions.md +65 -0
- data/docs/guides/performance.md +237 -0
- data/docs/guides/search.md +56 -0
- data/exe/jmespath +5 -0
- data/exe/jmespath.rb +5 -0
- data/lib/jmespath/ast.rb +43 -0
- data/lib/jmespath/cache.rb +53 -0
- data/lib/jmespath/cli.rb +36 -0
- data/lib/jmespath/data_access.rb +66 -0
- data/lib/jmespath/errors.rb +53 -0
- data/lib/jmespath/evaluation/construction.rb +20 -0
- data/lib/jmespath/evaluation/logic.rb +42 -0
- data/lib/jmespath/evaluation/projections.rb +43 -0
- data/lib/jmespath/evaluation/scalars.rb +49 -0
- data/lib/jmespath/evaluation/slices.rb +50 -0
- data/lib/jmespath/evaluator.rb +37 -0
- data/lib/jmespath/expression.rb +63 -0
- data/lib/jmespath/expression_reference.rb +15 -0
- data/lib/jmespath/functions/coercions.rb +41 -0
- data/lib/jmespath/functions/collections.rb +84 -0
- data/lib/jmespath/functions/conversion.rb +32 -0
- data/lib/jmespath/functions/function_call.rb +32 -0
- data/lib/jmespath/functions/references.rb +62 -0
- data/lib/jmespath/functions/scalar.rb +74 -0
- data/lib/jmespath/functions/signature.rb +77 -0
- data/lib/jmespath/functions.rb +73 -0
- data/lib/jmespath/input.rb +19 -0
- data/lib/jmespath/lexer.rb +132 -0
- data/lib/jmespath/normalizer.rb +53 -0
- data/lib/jmespath/options.rb +67 -0
- data/lib/jmespath/parser/expressions.rb +70 -0
- data/lib/jmespath/parser/projections.rb +91 -0
- data/lib/jmespath/parser.rb +119 -0
- data/lib/jmespath/plan.rb +153 -0
- data/lib/jmespath/token.rb +6 -0
- data/lib/jmespath/types.rb +84 -0
- data/lib/jmespath/version.rb +7 -0
- data/lib/jmespath.rb +100 -0
- data/sig/jmespath.rbs +55 -0
- metadata +135 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Functions
|
|
2
|
+
|
|
3
|
+
Function arguments are evaluated before type validation.
|
|
4
|
+
Expression references, written as `&expression`, defer evaluation until a function supplies an input.
|
|
5
|
+
|
|
6
|
+
## Standard functions
|
|
7
|
+
|
|
8
|
+
| Function | Accepted arguments | Result |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `abs` | number | Absolute value |
|
|
11
|
+
| `avg` | array of numbers | Mean, or nil for an empty array |
|
|
12
|
+
| `ceil` | number | Integer rounded upward |
|
|
13
|
+
| `floor` | number | Integer rounded downward |
|
|
14
|
+
| `contains` | string or array, value | Membership boolean |
|
|
15
|
+
| `ends_with` | string, string | Suffix boolean |
|
|
16
|
+
| `starts_with` | string, string | Prefix boolean |
|
|
17
|
+
| `length` | string, array, or object | Character or element count |
|
|
18
|
+
| `reverse` | string or array | Reversed value |
|
|
19
|
+
| `join` | string separator, array of strings | Joined string |
|
|
20
|
+
| `keys` | object | String keys |
|
|
21
|
+
| `values` | object | Values |
|
|
22
|
+
| `merge` | one or more objects | Object with later values winning |
|
|
23
|
+
| `map` | expression reference, array | Mapped values, including nil |
|
|
24
|
+
| `min` | homogeneous array of numbers or strings | Minimum, or nil |
|
|
25
|
+
| `max` | homogeneous array of numbers or strings | Maximum, or nil |
|
|
26
|
+
| `sort` | homogeneous array of numbers or strings | Stable sorted array |
|
|
27
|
+
| `sort_by` | array, expression reference | Stable array sorted by homogeneous keys |
|
|
28
|
+
| `min_by` | array, expression reference | Input element with minimum key, or nil |
|
|
29
|
+
| `max_by` | array, expression reference | Input element with maximum key, or nil |
|
|
30
|
+
| `sum` | array of numbers | Sum, including zero for an empty array |
|
|
31
|
+
| `not_null` | one or more values | First non-nil value |
|
|
32
|
+
| `to_array` | value | Array, preserving arrays |
|
|
33
|
+
| `to_number` | value | Number when convertible, otherwise nil |
|
|
34
|
+
| `to_string` | value | String or compact JSON representation |
|
|
35
|
+
| `type` | value | JMESPath type name |
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
JMESPath.search("map(&name, @)", [{name: "Ada"}, {}]) #=> ["Ada", nil]
|
|
39
|
+
JMESPath.search("sort_by(@, &age)[*].name", [{name: "Lin", age: 20}, {name: "Ada", age: 10}]) #=> ["Ada", "Lin"]
|
|
40
|
+
JMESPath.search("avg(@)", [1, 2, 3]) #=> 2.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Custom signatures
|
|
44
|
+
|
|
45
|
+
Start with `Functions.default` to retain the built-ins, or `Functions.new` for an empty registry.
|
|
46
|
+
Use an array of accepted type symbols for each argument.
|
|
47
|
+
Type symbols are `:any`, `:null`, `:boolean`, `:number`, `:string`, `:array`, `:object`, and `:expref`.
|
|
48
|
+
Typed arrays use `:array_number` or `:array_string`.
|
|
49
|
+
A union of those array types accepts either homogeneous array, including an empty array.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
functions = JMESPath::Functions.default.with("size_or_zero", signature: [[:string, :null]]) do |value|
|
|
53
|
+
value.nil? ? 0 : value.length
|
|
54
|
+
end
|
|
55
|
+
JMESPath.search("size_or_zero(missing)", {}, functions: functions) #=> 0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With `variadic: true`, the final argument signature repeats and remains required.
|
|
59
|
+
Every supplied argument is checked.
|
|
60
|
+
Duplicate names require `override: true`.
|
|
61
|
+
Registration returns a new frozen registry and leaves the original intact.
|
|
62
|
+
|
|
63
|
+
Callbacks receive evaluated Ruby values and may retain selected object identity.
|
|
64
|
+
Return a supported value and avoid mutating shared input.
|
|
65
|
+
Callback exceptions propagate with their original object and backtrace.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
Compile an expression explicitly when evaluating it against many documents.
|
|
4
|
+
The convenience cache avoids parsing repeated queries, while explicit compilation also avoids cache lookup.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
expression = JMESPath.compile("items[*].price | sum(@)")
|
|
8
|
+
expression.search({items: [{price: 10}, {price: 20}]}) #=> 30
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Reproduce
|
|
12
|
+
|
|
13
|
+
Prepare a checkout of [jmespath.rb at commit 5e86ea7](https://github.com/jmespath/jmespath.rb/tree/5e86ea7da2e954cc12f5b3a8383a43c0a65acba7).
|
|
14
|
+
The harness performs no downloads or installs.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
bundle exec ruby script/benchmark.rb --ruby-root /path/to/jmespath.rb --iterations 2000 --rounds 5 --memory --output /tmp/jmespath-benchmark.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The default suite includes all 12 cases from the four upstream benchmark files and six supplemental workloads.
|
|
21
|
+
Upstream files remain unchanged; `benchmark/upstream/sources.json` records their commit, license, and SHA-256 hashes.
|
|
22
|
+
Each workload runs in startup, repeated search, cached runtime reuse, direct compiled reuse, and cache-churn modes.
|
|
23
|
+
Use `--only field` to select one workload.
|
|
24
|
+
|
|
25
|
+
Cached runtime reuse compares `Expression#search` with the reference's persistent `Runtime#search`, following its `benchmark:cached` task.
|
|
26
|
+
Direct compiled reuse compares `Expression#search` with the reference's optimized node, bypassing its runtime.
|
|
27
|
+
|
|
28
|
+
The `--memory` option requires macOS `/usr/bin/time -l` and `ps`.
|
|
29
|
+
Omit that option to measure timing and allocations without the macOS memory tools.
|
|
30
|
+
Startup timing with `--memory` includes memory reporting; omit it when measuring cold latency alone.
|
|
31
|
+
|
|
32
|
+
## Measurements
|
|
33
|
+
|
|
34
|
+
These measurements compare revision `367f256e81f31ebff698466b768da0164610dd03` with the pinned jmespath.rb reference on arm64 macOS.
|
|
35
|
+
Each run uses default GC settings, disabled YJIT, and no Bundler.
|
|
36
|
+
Each sample performs 320 warmup evaluations and 2,000 measured evaluations, except startup samples.
|
|
37
|
+
Three paired samples alternate implementation order in separate processes using the same Ruby interpreter.
|
|
38
|
+
|
|
39
|
+
All 18 workloads have lower median times in repeated search, cached runtime reuse, and cache churn on each tested Ruby version.
|
|
40
|
+
Speedup is reference time divided by library time; larger values indicate faster execution.
|
|
41
|
+
Peak RAM includes process startup and warmup.
|
|
42
|
+
|
|
43
|
+
| MRI | Repeated search | Cached runtime reuse | Cache churn | Largest peak RAM increase across these modes |
|
|
44
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
45
|
+
| 3.3.11 | 1.40x to 86.51x | 1.10x to 79.11x | 1.19x to 2.48x | 2.36 MiB (12.3%) |
|
|
46
|
+
| 3.4.9 | 1.38x to 85.23x | 1.11x to 76.47x | 1.19x to 2.48x | 3.52 MiB (17.4%) |
|
|
47
|
+
| 4.0.9 | 1.35x to 74.36x | 1.04x to 70.93x | 1.16x to 2.27x | 1.25 MiB (5.4%) |
|
|
48
|
+
|
|
49
|
+
Cached runtime reuse follows the upstream cached benchmark: this library reuses an `Expression`, and the reference reuses a `Runtime`.
|
|
50
|
+
The implementations return the same checked results, including the upstream cases and six supplemental workloads.
|
|
51
|
+
|
|
52
|
+
Raw samples, runtime metadata, and source fingerprints are available for
|
|
53
|
+
[MRI 3.3.11](../../benchmark/results/mri-3.3.11-speed.json),
|
|
54
|
+
[MRI 3.4.9](../../benchmark/results/mri-3.4.9-speed.json), and
|
|
55
|
+
[MRI 4.0.9](../../benchmark/results/mri-4.0.9-speed.json).
|
|
56
|
+
|
|
57
|
+
Longer checks use 20,000 evaluations, five paired samples, and no memory collection.
|
|
58
|
+
Cached `wildcard_with_field_match2` is 1.10x faster on MRI 4.0.9; cached `multi_wildcard_field` is 1.44x faster on MRI 3.3.11.
|
|
59
|
+
The [follow-up samples](../../benchmark/results/cached-margin-checks.json) check the smallest cached speed margin and a noisy sample from the full runs.
|
|
60
|
+
|
|
61
|
+
### Saved query options
|
|
62
|
+
|
|
63
|
+
Revision `bb075c70dd86cd52d6b91c6ab2acb0f9f6e916a6` adds saved evaluation options to compiled queries.
|
|
64
|
+
These measurements compare saved options with per-call options on that revision and on the preceding implementation, `f73c7e4`.
|
|
65
|
+
Each workload uses five alternating samples, 640 warmup searches, and 10,000 measured searches in separate processes.
|
|
66
|
+
All runs use default GC settings, disabled YJIT, no Bundler, and arm64 macOS.
|
|
67
|
+
Results are checked before and after timing.
|
|
68
|
+
|
|
69
|
+
| MRI | Speedup over preceding per-call options | Speedup over same-revision per-call options | Largest peak RAM increase over preceding version |
|
|
70
|
+
| --- | ---: | ---: | ---: |
|
|
71
|
+
| 3.3.11 | 1.57x to 7.49x | 1.59x to 7.85x | 2.48 MiB |
|
|
72
|
+
| 3.4.9 | 1.55x to 8.25x | 1.58x to 8.87x | 7.08 MiB |
|
|
73
|
+
| 4.0.9 | 1.49x to 6.41x | 1.49x to 6.73x | 2.08 MiB |
|
|
74
|
+
|
|
75
|
+
The seven cases cover field traversal, projection, a custom function, an unchanged built-in, a built-in override, and valid and invalid suppressed calls.
|
|
76
|
+
They use the `items` document in `benchmark/documents.json` and these configurations:
|
|
77
|
+
|
|
78
|
+
| Case | Expression | Saved options |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| Field | `meta.owner.name` | Registry extended with `double` |
|
|
81
|
+
| Projection | `items[*].price` | Registry extended with `double` |
|
|
82
|
+
| Custom | `double(items[3].price)` | Registry extended with `double` |
|
|
83
|
+
| Unchanged built-in | `length(text)` | Registry extended with `double` |
|
|
84
|
+
| Override | `length(text)` | Override `length` to return twice the string length |
|
|
85
|
+
| Suppressed | `length(text)` | `disable_visit_errors: true` |
|
|
86
|
+
| Invalid | `length(items[0].active)` | `disable_visit_errors: true` |
|
|
87
|
+
|
|
88
|
+
The `double` function accepts one number and returns twice its value; the override accepts one string.
|
|
89
|
+
On MRI 4.0, field lookup drops from six allocated objects per search in the preceding version to one with saved options.
|
|
90
|
+
Peak RAM includes process startup and warmup; it is measured with macOS `/usr/bin/time -l`.
|
|
91
|
+
The largest increase is projection on MRI 3.4: 27.66 MiB versus 20.58 MiB.
|
|
92
|
+
Saving options reduces temporary allocations, but the default GC schedule can retain more array memory between collections.
|
|
93
|
+
|
|
94
|
+
Default search controls cover all 18 existing workloads.
|
|
95
|
+
Their median speed ratios range from 0.99x to 1.09x for public search and 0.97x to 1.10x for compiled search, excluding one short-run outlier.
|
|
96
|
+
That MRI 3.3 deep-query outlier initially measured 0.94x; seven paired 100,000-search samples measured 1.001x.
|
|
97
|
+
This does not establish a regression in that workload.
|
|
98
|
+
|
|
99
|
+
Raw samples and source fingerprints: [MRI 3.3.11](../../benchmark/results/configured-mri-3.3.11.json),
|
|
100
|
+
[MRI 3.4.9](../../benchmark/results/configured-mri-3.4.9.json), [MRI 4.0.9](../../benchmark/results/configured-mri-4.0.9.json),
|
|
101
|
+
and the [deep-query recheck](../../benchmark/results/configured-deep-recheck.json).
|
|
102
|
+
|
|
103
|
+
### Direct-node diagnostics
|
|
104
|
+
|
|
105
|
+
The `compiled` mode calls the reference's private optimized node directly, bypassing its runtime API.
|
|
106
|
+
This library still calls `Expression#search`, which supports per-call options and input adapters.
|
|
107
|
+
This stricter comparison does not show a speedup for every workload:
|
|
108
|
+
|
|
109
|
+
| MRI | Workloads faster | Workloads without a median speedup |
|
|
110
|
+
| --- | ---: | --- |
|
|
111
|
+
| 3.3.11 | 17 of 18 | `single_expression` |
|
|
112
|
+
| 3.4.9 | 17 of 18 | `single_expression` |
|
|
113
|
+
| 4.0.9 | 15 of 18 | `single_expression`, `wildcard_with_field_match`, `wildcard_with_field_match2` |
|
|
114
|
+
|
|
115
|
+
Startup samples include memory-reporting overhead and do not establish pure load-time improvements.
|
|
116
|
+
|
|
117
|
+
## Earlier measurements
|
|
118
|
+
|
|
119
|
+
These workload-specific measurements compare revision `2d061978319b7757443e52c5aba01992ab8c34b0` with jmespath.rb 1.6.2 on MRI 4.0.9, arm64 macOS, with YJIT disabled.
|
|
120
|
+
Each sample performs 320 warmup evaluations and 2,000 measured evaluations, except startup samples, which perform one evaluation without warmup.
|
|
121
|
+
Five paired samples alternate implementation order.
|
|
122
|
+
Implementations run in separate processes using the same Ruby interpreter and Bundler environment.
|
|
123
|
+
Expected results are checked before and after timing; startup checks its result after timing.
|
|
124
|
+
The harness rejects source or fixture changes during a run.
|
|
125
|
+
|
|
126
|
+
Raw samples, runtime details, and source fingerprints are in [mri-4.0.9-optimized.json](../../benchmark/results/mri-4.0.9-optimized.json).
|
|
127
|
+
The earlier, unoptimized baseline remains in [mri-4.0.9.json](../../benchmark/results/mri-4.0.9.json).
|
|
128
|
+
|
|
129
|
+
Search speedup is reference time divided by library time; values above 1 indicate faster execution.
|
|
130
|
+
Tables show median allocations per 2,000 searches and whole-process peak resident memory, including startup and warmup.
|
|
131
|
+
Memory collection occurs outside the timed search loop.
|
|
132
|
+
Retained Ruby object bytes are also recorded after full garbage collection; they exclude some native allocations and are not process RAM.
|
|
133
|
+
Allocation counts alone do not establish lower RAM usage.
|
|
134
|
+
|
|
135
|
+
18 of 18 workloads were faster; 17 had equal or lower median peak RAM.
|
|
136
|
+
|
|
137
|
+
### Upstream workloads
|
|
138
|
+
|
|
139
|
+
| Workload | Search speedup | Peak RAM, library / reference (MiB) | Allocations, library / reference |
|
|
140
|
+
| --- | ---: | ---: | ---: |
|
|
141
|
+
| single_expression | 2.70x | 29.12 / 29.88 | 6007 / 46011 |
|
|
142
|
+
| single_dot_expression | 4.81x | 28.97 / 29.64 | 6007 / 102015 |
|
|
143
|
+
| double_dot_expression | 6.98x | 29.02 / 29.67 | 6007 / 148017 |
|
|
144
|
+
| dot_no_match | 7.97x | 29.02 / 29.86 | 6007 / 196023 |
|
|
145
|
+
| deep_nesting_10 | 17.10x | 29.08 / 30.02 | 6007 / 526029 |
|
|
146
|
+
| deep_nesting_50 | 57.57x | 29.30 / 30.78 | 6007 / 4546109 |
|
|
147
|
+
| deep_nesting_50_pipe | 56.31x | 29.39 / 31.00 | 6007 / 4546109 |
|
|
148
|
+
| deep_nesting_50_index | 28.36x | 29.25 / 30.11 | 6007 / 4950009 |
|
|
149
|
+
| multi_wildcard_field | 4.59x | 29.16 / 29.92 | 12007 / 166041 |
|
|
150
|
+
| wildcard_with_index | 5.81x | 29.03 / 29.91 | 8007 / 222051 |
|
|
151
|
+
| wildcard_with_field_match | 3.83x | 29.19 / 30.08 | 8007 / 104020 |
|
|
152
|
+
| wildcard_with_field_match2 | 4.18x | 29.03 / 29.98 | 8007 / 110028 |
|
|
153
|
+
|
|
154
|
+
### Supplemental workloads
|
|
155
|
+
|
|
156
|
+
| Workload | Search speedup | Peak RAM, library / reference (MiB) | Allocations, library / reference |
|
|
157
|
+
| --- | ---: | ---: | ---: |
|
|
158
|
+
| field | 7.29x | 28.84 / 29.72 | 6007 / 156016 |
|
|
159
|
+
| projection | 1.61x | 30.80 / 30.23 | 8007 / 112032 |
|
|
160
|
+
| filter | 1.44x | 30.05 / 30.39 | 8007 / 126016 |
|
|
161
|
+
| sort | 1.18x | 29.75 / 29.88 | 226007 / 460012 |
|
|
162
|
+
| slice | 3.55x | 29.50 / 29.83 | 14007 / 174018 |
|
|
163
|
+
| unicode | 4.80x | 29.31 / 29.84 | 22007 / 204015 |
|
|
164
|
+
|
|
165
|
+
### Compiled reuse and cache churn
|
|
166
|
+
|
|
167
|
+
Compiled reuse: 2 of 18 workloads were faster; 17 had equal or lower median peak RAM.
|
|
168
|
+
Cache churn: 18 of 18 workloads were faster; 11 had equal or lower median peak RAM.
|
|
169
|
+
|
|
170
|
+
The reference compiled baseline visits its optimized tree through a private API because it has no public `compile` equivalent.
|
|
171
|
+
`Expression#search` also supports per-call options and input adapters.
|
|
172
|
+
These operations are reported separately from the public `JMESPath.search` comparison.
|
|
173
|
+
|
|
174
|
+
Churn cycles through 320 distinct expressions, exceeding the library's 32-entry cache.
|
|
175
|
+
The reference constructs a runtime and parser for each public search, so its cache does not persist between those calls.
|
|
176
|
+
This mode tests the same query stream against each implementation's public behavior.
|
|
177
|
+
|
|
178
|
+
### Default GC across supported MRI series
|
|
179
|
+
|
|
180
|
+
The earlier implementation did not meet strict peak-RAM parity on MRI 3.3, 3.4, and 4.0.
|
|
181
|
+
Public search is faster in all 18 workloads on each tested version, but some workloads use more peak RAM.
|
|
182
|
+
The implementation remains pure Ruby; these comparisons exclude application-level GC tuning.
|
|
183
|
+
|
|
184
|
+
Additional runs use MRI 3.3.11 and 3.4.9 without Bundler, with default GC settings and YJIT disabled.
|
|
185
|
+
Each run uses the same library source, 320 warmup evaluations, 2,000 measured evaluations, and three alternating paired samples.
|
|
186
|
+
Compare implementations within each run; the MRI 4.0.9 results above use a different process environment and sample count.
|
|
187
|
+
|
|
188
|
+
| MRI | Search speedup range | Search RAM target met | Compiled workloads faster | Churn RAM target met |
|
|
189
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
190
|
+
| 3.3.11 | 1.14x to 61.58x | 14 of 18 | 7 of 18 | 15 of 18 |
|
|
191
|
+
| 3.4.9 | 1.21x to 60.33x | 17 of 18 | 6 of 18 | 5 of 18 |
|
|
192
|
+
|
|
193
|
+
Churn is faster in all 18 workloads on both versions.
|
|
194
|
+
Raw results include every mode and sample: [MRI 3.3.11](../../benchmark/results/mri-3.3.11-defaults.json) and [MRI 3.4.9](../../benchmark/results/mri-3.4.9-defaults.json).
|
|
195
|
+
Repeat either run with its installed Ruby interpreter:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
ruby script/benchmark.rb --ruby-root /path/to/jmespath.rb --iterations 2000 --rounds 3 --memory --output /tmp/jmespath-defaults.json
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
A separate projection diagnostic uses 640 warmup evaluations, 20,000 measured searches, and three paired samples without Bundler.
|
|
202
|
+
The table reports median peak RAM and collections during the measured loop.
|
|
203
|
+
|
|
204
|
+
| MRI | Search speedup | Peak RAM, library / reference (MiB) | GC collections, library / reference |
|
|
205
|
+
| --- | ---: | ---: | ---: |
|
|
206
|
+
| 3.3.11 | 1.72x | 19.28 / 18.36 | 12 / 173 |
|
|
207
|
+
| 3.4.9 | 1.70x | 20.97 / 19.50 | 8 / 95 |
|
|
208
|
+
| 4.0.9 | 1.61x | 22.44 / 21.14 | 10 / 96 |
|
|
209
|
+
|
|
210
|
+
Retained Ruby object bytes are lower in all three diagnostic runs, while peak RAM is 5% to 8% higher.
|
|
211
|
+
Native snapshots on MRI 4.0.9 show array memory returning to baseline after full GC, while about 1 MiB of extra physical footprint remains.
|
|
212
|
+
This supports temporary array accumulation and native allocator retention as causes of the gap in that workload.
|
|
213
|
+
|
|
214
|
+
Three isolated alternatives failed to meet both targets.
|
|
215
|
+
Growing projection arrays incrementally leaves peak RAM 7% to 9% higher and reduces speed.
|
|
216
|
+
An eight-entry cache leaves peak RAM 14% higher in the deep-query churn case on MRI 3.4.9.
|
|
217
|
+
Uncached public search makes the projection case 5% slower than the reference, with peak RAM 0.4% higher.
|
|
218
|
+
These experimental alternatives were not adopted.
|
|
219
|
+
The [diagnostic report](../../benchmark/results/default-gc-diagnosis.json) preserves raw samples, source fingerprints, and native memory snapshots.
|
|
220
|
+
|
|
221
|
+
## Optimization decisions
|
|
222
|
+
|
|
223
|
+
Default searches reuse immutable configuration and compiled operations.
|
|
224
|
+
Field readers share the data-access implementation.
|
|
225
|
+
Slices prepare their bounds and direction once per compiled expression.
|
|
226
|
+
Native string length and reversal bypass generic function dispatch.
|
|
227
|
+
Function arguments and expression-reference callbacks use compiled operations; function signatures remain checked.
|
|
228
|
+
Operations without a compiled handler use the evaluator.
|
|
229
|
+
Compiled expressions can save a custom registry and error policy while retaining compiled operations.
|
|
230
|
+
Field dispatch avoids the generic handler lookup.
|
|
231
|
+
Projections compact a single result array, and stable sorting uses one key-and-index pair per item.
|
|
232
|
+
Whitespace runs are scanned together.
|
|
233
|
+
The cache retains 32 expressions, with the existing 1,048,576-byte source limit.
|
|
234
|
+
|
|
235
|
+
An experimental cache removed default-hit allocations but increased peak RAM by 32% in a 20,000-search projection comparison.
|
|
236
|
+
That variant was rejected.
|
|
237
|
+
Ruby's garbage-collection schedule can make fewer allocations coexist with higher peak RAM; compare both metrics on representative workloads.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Search semantics
|
|
2
|
+
|
|
3
|
+
JMESPath expressions select and transform structured values.
|
|
4
|
+
The [language specification](https://jmespath.org/specification.html) defines the grammar and standard functions.
|
|
5
|
+
|
|
6
|
+
## Fields and indices
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
JMESPath.search("user.name", {user: {name: "Ada"}}) #=> "Ada"
|
|
10
|
+
JMESPath.search("items[-1]", {items: [10, 20, 30]}) #=> 30
|
|
11
|
+
JMESPath.search("items[99]", {items: [10]}) #=> nil
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Hash fields accept string and existing symbol keys.
|
|
15
|
+
A non-nil string value wins when both forms exist.
|
|
16
|
+
A nil string value allows fallback to the symbol key.
|
|
17
|
+
Struct lookup visits members, not arbitrary methods.
|
|
18
|
+
|
|
19
|
+
## Projections and flattening
|
|
20
|
+
|
|
21
|
+
A projection applies its right expression to each input element.
|
|
22
|
+
Nil results are omitted; false results remain present.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
JMESPath.search("[*].value", [{value: 1}, {}, {value: false}]) #=> [1, false]
|
|
26
|
+
JMESPath.search("[]", [[1, 2], [3, [4]]]) #=> [1, 2, 3, [4]]
|
|
27
|
+
JMESPath.search("[*].value | [0]", [{value: 1}, {value: 2}]) #=> 1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Flattening removes one array level.
|
|
31
|
+
A pipe evaluates its right expression against the entire left result, ending projection propagation.
|
|
32
|
+
|
|
33
|
+
## Filters and truthiness
|
|
34
|
+
|
|
35
|
+
False, nil, empty strings, empty arrays, and empty objects are false values.
|
|
36
|
+
Zero is true in JMESPath.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
JMESPath.search("items[?enabled].id", {items: [{id: 1, enabled: true}, {id: 2, enabled: false}]}) #=> [1]
|
|
40
|
+
JMESPath.search("a || b", {a: [], b: "fallback"}) #=> "fallback"
|
|
41
|
+
JMESPath.search("!value", {value: 0}) #=> false
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Boolean operators short-circuit and return the selected operand.
|
|
45
|
+
|
|
46
|
+
## Slices and constructed values
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
JMESPath.search("[::-1]", [1, 2, 3]) #=> [3, 2, 1]
|
|
50
|
+
JMESPath.search("[:0:-1]", [1, 2, 3]) #=> [3, 2]
|
|
51
|
+
JMESPath.search("{name: user.name, missing: absent}", {user: {name: "Ada"}}) #=> {"name" => "Ada", "missing" => nil}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
An omitted negative-slice stop differs from an explicit `-1`.
|
|
55
|
+
A zero slice step raises InvalidValueError.
|
|
56
|
+
Multiselects construct new arrays or hashes and retain explicit nil values.
|
data/exe/jmespath
ADDED
data/exe/jmespath.rb
ADDED
data/lib/jmespath/ast.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
module AST
|
|
6
|
+
# Immutable syntax and literal templates.
|
|
7
|
+
class Node
|
|
8
|
+
attr_reader :kind, :value, :children, :start, :finish, :depth
|
|
9
|
+
|
|
10
|
+
def initialize(kind:, value: nil, children: [], start: 0, finish: 0)
|
|
11
|
+
@kind = kind
|
|
12
|
+
@value = self.class.immutable(value)
|
|
13
|
+
@children = children.freeze
|
|
14
|
+
@start = start
|
|
15
|
+
@finish = finish
|
|
16
|
+
@depth = if kind == :literal
|
|
17
|
+
self.class.literal_depth(value)
|
|
18
|
+
else
|
|
19
|
+
(children.map(&:depth).max || -1) + 1
|
|
20
|
+
end
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.literal_depth(value)
|
|
25
|
+
values = case value
|
|
26
|
+
when Array then value
|
|
27
|
+
when Hash then value.values
|
|
28
|
+
else return 0
|
|
29
|
+
end
|
|
30
|
+
1 + (values.map { |entry| literal_depth(entry) }.max || 0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.immutable(value)
|
|
34
|
+
case value
|
|
35
|
+
when Array then value.map { |item| immutable(item) }.freeze
|
|
36
|
+
when Hash then value.transform_values { |item| immutable(item) }.freeze
|
|
37
|
+
when String then value.dup.freeze
|
|
38
|
+
else value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Cache
|
|
6
|
+
def initialize(limit: 32, bytes: 1_048_576)
|
|
7
|
+
raise ArgumentError, "Cache capacities must be positive" unless limit.positive? && bytes.positive?
|
|
8
|
+
|
|
9
|
+
@limit = limit
|
|
10
|
+
@budget = bytes
|
|
11
|
+
@used = 0
|
|
12
|
+
@entries = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch(source, max_depth:)
|
|
17
|
+
return yield source if source.bytesize > @budget
|
|
18
|
+
|
|
19
|
+
# @type var key: [String, Integer]
|
|
20
|
+
key = [source.frozen? ? source : source.dup.freeze, max_depth].freeze
|
|
21
|
+
cached = @mutex.synchronize { read(key) }
|
|
22
|
+
return cached if cached
|
|
23
|
+
|
|
24
|
+
value = yield key.first
|
|
25
|
+
@mutex.synchronize { write(key, value) }
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def read(key)
|
|
32
|
+
value = @entries.delete(key)
|
|
33
|
+
@entries[key] = value if value
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def write(key, value)
|
|
38
|
+
@used -= key.first.bytesize if @entries.delete(key)
|
|
39
|
+
evict_for(key.first.bytesize)
|
|
40
|
+
@entries[key] = value
|
|
41
|
+
@used += key.first.bytesize
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def evict_for(size)
|
|
45
|
+
@entries.each_key do |key|
|
|
46
|
+
break if @entries.length < @limit && @used + size <= @budget
|
|
47
|
+
|
|
48
|
+
@entries.delete(key)
|
|
49
|
+
@used -= key.first.bytesize
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/jmespath/cli.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../jmespath"
|
|
4
|
+
|
|
5
|
+
module JMESPath
|
|
6
|
+
# Runs one JMESPath query against a JSON document from standard input.
|
|
7
|
+
module CLI
|
|
8
|
+
USAGE = "Usage: jmespath EXPRESSION < document.json"
|
|
9
|
+
|
|
10
|
+
# @param argv [Array<String>] command-line arguments
|
|
11
|
+
# @param stdin [IO, StringIO] source JSON stream
|
|
12
|
+
# @param stdout [IO, StringIO] result stream
|
|
13
|
+
# @param stderr [IO, StringIO] diagnostic stream
|
|
14
|
+
# @return [Integer] zero on success, one on input failure, two on usage failure
|
|
15
|
+
def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
|
|
16
|
+
return information(argv, stdout) if [%w[--help], %w[-h], %w[--version]].include?(argv)
|
|
17
|
+
|
|
18
|
+
unless argv.length == 1
|
|
19
|
+
stderr.puts(USAGE)
|
|
20
|
+
return 2
|
|
21
|
+
end
|
|
22
|
+
data = JSON.parse(stdin.read.to_s)
|
|
23
|
+
stdout.puts(JSON.generate(JMESPath.search(argv.fetch(0), data)))
|
|
24
|
+
0
|
|
25
|
+
rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
|
|
26
|
+
stderr.puts(e.message)
|
|
27
|
+
1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
def self.information(argv, output)
|
|
32
|
+
output.puts(argv == ["--version"] ? "jmespath-ruby #{VERSION}" : USAGE)
|
|
33
|
+
0
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
module DataAccess
|
|
6
|
+
# @api private
|
|
7
|
+
module FieldLookup
|
|
8
|
+
def field(value, name = @field_name)
|
|
9
|
+
mapping = value.is_a?(Hash) ? value : DataAccess.object(value)
|
|
10
|
+
return nil unless mapping
|
|
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
|
|
17
|
+
end
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
extend FieldLookup
|
|
23
|
+
|
|
24
|
+
# @api private
|
|
25
|
+
class FieldReader
|
|
26
|
+
include FieldLookup
|
|
27
|
+
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@field_name = name
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
alias call field
|
|
34
|
+
|
|
35
|
+
private :field
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def array(value)
|
|
40
|
+
convert(value, Array, :to_ary)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def object(value)
|
|
44
|
+
return value.to_h if value.is_a?(Struct)
|
|
45
|
+
|
|
46
|
+
convert(value, Hash, :to_hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def string(value)
|
|
50
|
+
convert(value, String, :to_str)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def convert(value, type, method)
|
|
56
|
+
return value if value.is_a?(type)
|
|
57
|
+
return nil unless value.respond_to?(method)
|
|
58
|
+
|
|
59
|
+
result = value.public_send(method)
|
|
60
|
+
return result if result.nil? || result.is_a?(type)
|
|
61
|
+
|
|
62
|
+
raise TypeError, "#{method} must return #{type}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# Failures reported by the query language.
|
|
5
|
+
module Errors
|
|
6
|
+
# Base class for JMESPath failures.
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
# A runtime evaluation failure.
|
|
9
|
+
class RuntimeError < Error; end
|
|
10
|
+
|
|
11
|
+
# A malformed expression with a byte position and readable coordinates.
|
|
12
|
+
class SyntaxError < Error
|
|
13
|
+
# @return [String] the expression text
|
|
14
|
+
attr_reader :expression
|
|
15
|
+
# @return [Integer] zero-based byte offset
|
|
16
|
+
attr_reader :position
|
|
17
|
+
|
|
18
|
+
# @param message [String] the reason parsing failed
|
|
19
|
+
# @param expression [String] expression text
|
|
20
|
+
# @param position [Integer] zero-based byte offset
|
|
21
|
+
def initialize(message, expression: "", position: 0)
|
|
22
|
+
@expression = expression.dup.freeze
|
|
23
|
+
@position = position
|
|
24
|
+
super("#{message} at byte #{position}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Integer] one-based line number
|
|
28
|
+
def line
|
|
29
|
+
prefix.count("\n") + 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Integer] one-based character column
|
|
33
|
+
def column
|
|
34
|
+
(prefix.split("\n", -1).last || "").length + 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def prefix
|
|
40
|
+
expression.byteslice(0, position).to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A function received an unsupported value type.
|
|
45
|
+
class InvalidTypeError < RuntimeError; end
|
|
46
|
+
# A value is invalid for the requested operation.
|
|
47
|
+
class InvalidValueError < RuntimeError; end
|
|
48
|
+
# A function received an incorrect number of arguments.
|
|
49
|
+
class InvalidArityError < RuntimeError; end
|
|
50
|
+
# A function name is not registered.
|
|
51
|
+
class UnknownFunctionError < RuntimeError; end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JMESPath
|
|
4
|
+
# @api private
|
|
5
|
+
class Evaluator
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def list(node, data)
|
|
9
|
+
return nil if data.nil?
|
|
10
|
+
|
|
11
|
+
node.children.map { |child| evaluate(child, data) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def mapping(node, data)
|
|
15
|
+
return nil if data.nil?
|
|
16
|
+
|
|
17
|
+
node.children.to_h { |pair| [pair.value.to_s, evaluate(pair.children.fetch(0), data)] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|